1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
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/>.
34 from xml
.sax
import saxutils
36 from threading
import Event
37 from threading
import Thread
38 from threading
import Semaphore
39 from string
import strip
42 from gpodder
import util
43 from gpodder
import opml
44 from gpodder
import services
45 from gpodder
import sync
46 from gpodder
import download
47 from gpodder
import SimpleGladeApp
48 from gpodder
import my
49 from gpodder
.liblogger
import log
50 from gpodder
.dbsqlite
import db
51 from gpodder
import resolver
54 from gpodder
import trayicon
56 except Exception, exc
:
57 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
58 log('Warning: This probably means your PyGTK installation is too old!')
61 from libpodcasts
import podcastChannel
62 from libpodcasts
import LocalDBReader
63 from libpodcasts
import podcastItem
64 from libpodcasts
import channels_to_model
65 from libpodcasts
import update_channel_model_by_iter
66 from libpodcasts
import load_channels
67 from libpodcasts
import update_channels
68 from libpodcasts
import save_channels
69 from libpodcasts
import can_restore_from_opml
70 from libpodcasts
import HTTPAuthError
72 from gpodder
.libgpodder
import gl
74 from libplayers
import UserAppsReader
76 from libtagupdate
import tagging_supported
78 if gpodder
.interface
== gpodder
.GUI
:
79 WEB_BROWSER_ICON
= 'web-browser'
80 elif gpodder
.interface
== gpodder
.MAEMO
:
82 WEB_BROWSER_ICON
= 'qgn_toolb_browser_web'
85 app_version
= "unknown" # will be set in main() call
87 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
89 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
90 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
91 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
92 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
93 'Carlos Moffat', 'Chris Arnold', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
94 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'FFranci72', 'Florian Richter', 'Frank Harper',
95 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
96 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
97 'Jérôme Chabod', 'Jerry Moss',
98 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
99 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
100 'Jürgen Schinker', 'Justin Forest',
101 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
102 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Mykola Nikishov', 'narf',
103 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
104 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
105 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
106 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
107 'Robert Young', 'Roel Groeneveld',
108 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
109 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
110 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
111 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
112 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
114 'List may be incomplete - please contact me.'
116 app_copyright
= '© 2005-2008 Thomas Perl and the gPodder Team'
117 app_website
= 'http://www.gpodder.org/'
119 # these will be filled with pathnames in bin/gpodder
120 glade_dir
= [ 'share', 'gpodder' ]
121 icon_dir
= [ 'share', 'pixmaps', 'gpodder.png' ]
122 scalable_dir
= [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
125 class GladeWidget(SimpleGladeApp
.SimpleGladeApp
):
126 gpodder_main_window
= None
127 finger_friendly_widgets
= []
129 def __init__( self
, **kwargs
):
130 path
= os
.path
.join( glade_dir
, '%s.glade' % app_name
)
131 root
= self
.__class
__.__name
__
134 SimpleGladeApp
.SimpleGladeApp
.__init
__( self
, path
, root
, domain
, **kwargs
)
136 # Set widgets to finger-friendly mode if on Maemo
137 for widget_name
in self
.finger_friendly_widgets
:
138 if hasattr(self
, widget_name
):
139 self
.set_finger_friendly(getattr(self
, widget_name
))
141 log('Finger-friendly widget not found: %s', widget_name
, sender
=self
)
143 if root
== 'gPodder':
144 GladeWidget
.gpodder_main_window
= self
.gPodder
146 # If we have a child window, set it transient for our main window
147 getattr( self
, root
).set_transient_for( GladeWidget
.gpodder_main_window
)
149 if gpodder
.interface
== gpodder
.GUI
:
150 if hasattr( self
, 'center_on_widget'):
151 ( x
, y
) = self
.gpodder_main_window
.get_position()
152 a
= self
.center_on_widget
.allocation
153 ( x
, y
) = ( x
+ a
.x
, y
+ a
.y
)
154 ( w
, h
) = ( a
.width
, a
.height
)
155 ( pw
, ph
) = getattr( self
, root
).get_size()
156 getattr( self
, root
).move( x
+ w
/2 - pw
/2, y
+ h
/2 - ph
/2)
158 getattr( self
, root
).set_position( gtk
.WIN_POS_CENTER_ON_PARENT
)
160 def notification(self
, message
, title
=None):
161 util
.idle_add(self
.show_message
, message
, title
)
163 def show_message( self
, message
, title
= None):
164 if hasattr(self
, 'tray_icon') and hasattr(self
, 'minimized') and self
.tray_icon
and self
.minimized
:
167 self
.tray_icon
.send_notification(message
, title
)
170 if gpodder
.interface
== gpodder
.GUI
:
171 dlg
= gtk
.MessageDialog(GladeWidget
.gpodder_main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
)
173 dlg
.set_title(str(title
))
174 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
176 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
177 elif gpodder
.interface
== gpodder
.MAEMO
:
178 dlg
= hildon
.Note('information', (GladeWidget
.gpodder_main_window
, message
))
183 def set_finger_friendly(self
, widget
):
185 If we are on Maemo, we carry out the necessary
186 operations to turn a widget into a finger-friendly
187 one, depending on which type of widget it is (i.e.
188 buttons will have more padding, TreeViews a thick
191 if gpodder
.interface
== gpodder
.MAEMO
:
192 if isinstance(widget
, gtk
.Misc
):
193 widget
.set_padding(0, 5)
194 elif isinstance(widget
, gtk
.Button
):
195 for child
in widget
.get_children():
196 if isinstance(child
, gtk
.Alignment
):
197 child
.set_padding(5, 5, 5, 5)
199 child
.set_padding(5, 5)
200 elif isinstance(widget
, gtk
.TreeView
) or isinstance(widget
, gtk
.TextView
):
201 parent
= widget
.get_parent()
202 if isinstance(parent
, gtk
.ScrolledWindow
):
203 hildon
.hildon_helper_set_thumb_scrollbar(parent
, True)
204 elif isinstance(widget
, gtk
.MenuItem
):
205 for child
in widget
.get_children():
206 self
.set_finger_friendly(child
)
207 submenu
= widget
.get_submenu()
208 if submenu
is not None:
209 for child
in submenu
.get_children():
210 self
.set_finger_friendly(child
)
211 elif isinstance(widget
, gtk
.Menu
):
212 for child
in widget
.get_children():
213 self
.set_finger_friendly(child
)
215 log('Cannot set widget finger-friendly: %s', widget
, sender
=self
)
219 def show_confirmation( self
, message
, title
= None):
220 if gpodder
.interface
== gpodder
.GUI
:
221 affirmative
= gtk
.RESPONSE_YES
222 dlg
= gtk
.MessageDialog(GladeWidget
.gpodder_main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
)
224 dlg
.set_title(str(title
))
225 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
227 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
228 elif gpodder
.interface
== gpodder
.MAEMO
:
229 affirmative
= gtk
.RESPONSE_OK
230 dlg
= hildon
.Note('confirmation', (GladeWidget
.gpodder_main_window
, message
))
235 return response
== affirmative
237 def UsernamePasswordDialog( self
, title
, message
, username
=None, password
=None, username_prompt
=_('Username'), register_callback
=None):
238 """ An authentication dialog based on
239 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
241 dialog
= gtk
.MessageDialog(
242 GladeWidget
.gpodder_main_window
,
243 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
244 gtk
.MESSAGE_QUESTION
,
245 gtk
.BUTTONS_OK_CANCEL
)
247 dialog
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_DIALOG
))
249 dialog
.set_markup('<span weight="bold" size="larger">' + title
+ '</span>')
250 dialog
.set_title(_('Authentication required'))
251 dialog
.format_secondary_markup(message
)
252 dialog
.set_default_response(gtk
.RESPONSE_OK
)
254 if register_callback
is not None:
255 dialog
.add_button(_('New user'), gtk
.RESPONSE_HELP
)
257 username_entry
= gtk
.Entry()
258 password_entry
= gtk
.Entry()
260 username_entry
.connect('activate', lambda w
: password_entry
.grab_focus())
261 password_entry
.set_visibility(False)
262 password_entry
.set_activates_default(True)
264 if username
is not None:
265 username_entry
.set_text(username
)
266 if password
is not None:
267 password_entry
.set_text(password
)
269 table
= gtk
.Table(2, 2)
270 table
.set_row_spacings(6)
271 table
.set_col_spacings(6)
273 username_label
= gtk
.Label()
274 username_label
.set_markup('<b>' + username_prompt
+ ':</b>')
275 username_label
.set_alignment(0.0, 0.5)
276 table
.attach(username_label
, 0, 1, 0, 1, gtk
.FILL
, 0)
277 table
.attach(username_entry
, 1, 2, 0, 1)
279 password_label
= gtk
.Label()
280 password_label
.set_markup('<b>' + _('Password') + ':</b>')
281 password_label
.set_alignment(0.0, 0.5)
282 table
.attach(password_label
, 0, 1, 1, 2, gtk
.FILL
, 0)
283 table
.attach(password_entry
, 1, 2, 1, 2)
285 dialog
.vbox
.pack_end(table
, True, True, 0)
287 response
= dialog
.run()
289 while response
== gtk
.RESPONSE_HELP
:
291 response
= dialog
.run()
293 password_entry
.set_visibility(True)
296 return response
== gtk
.RESPONSE_OK
, ( username_entry
.get_text(), password_entry
.get_text() )
298 def show_copy_dialog( self
, src_filename
, dst_filename
= None, dst_directory
= None, title
= _('Select destination')):
299 if dst_filename
is None:
300 dst_filename
= src_filename
302 if dst_directory
is None:
303 dst_directory
= os
.path
.expanduser( '~')
305 ( base
, extension
) = os
.path
.splitext( src_filename
)
307 if not dst_filename
.endswith( extension
):
308 dst_filename
+= extension
310 if gpodder
.interface
== gpodder
.GUI
:
311 dlg
= gtk
.FileChooserDialog(title
=title
, parent
=GladeWidget
.gpodder_main_window
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
312 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
313 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
314 elif gpodder
.interface
== gpodder
.MAEMO
:
315 dlg
= hildon
.FileChooserDialog(GladeWidget
.gpodder_main_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
317 dlg
.set_do_overwrite_confirmation( True)
318 dlg
.set_current_name( os
.path
.basename( dst_filename
))
319 dlg
.set_current_folder( dst_directory
)
322 folder
= dst_directory
323 if dlg
.run() == gtk
.RESPONSE_OK
:
325 dst_filename
= dlg
.get_filename()
326 folder
= dlg
.get_current_folder()
327 if not dst_filename
.endswith( extension
):
328 dst_filename
+= extension
330 log( 'Copying %s => %s', src_filename
, dst_filename
, sender
= self
)
333 shutil
.copyfile( src_filename
, dst_filename
)
335 log( 'Error copying file.', sender
= self
, traceback
= True)
338 return (result
, folder
)
341 class gPodder(GladeWidget
):
342 finger_friendly_widgets
= ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'itemQuit', 'menuPodcasts', 'advanced1', 'menuChannels', 'menuHelp']
343 ENTER_URL_TEXT
= _('Enter podcast URL...')
346 if gpodder
.interface
== gpodder
.MAEMO
:
347 # Maemo-specific changes to the UI
349 scalable_dir
= scalable_dir
.replace('.svg', '.png')
351 self
.app
= hildon
.Program()
352 gtk
.set_application_name('gPodder')
353 self
.window
= hildon
.Window()
354 self
.window
.connect('delete-event', self
.on_gPodder_delete_event
)
355 self
.window
.connect('window-state-event', self
.window_state_event
)
357 self
.itemUpdateChannel
.show()
358 self
.UpdateChannelSeparator
.show()
360 # Give toolbar to the hildon window
361 self
.toolbar
.parent
.remove(self
.toolbar
)
362 self
.window
.add_toolbar(self
.toolbar
)
364 # START TEMPORARY FIX FOR TOOLBAR STYLE
365 # It seems like libglade for python still mixes
366 # old GtkToolbar API with new ones - maybe this
367 # is the reason why setting the style doesn't
368 # work very well. This small hack fixes that :)
369 self
.toolbar
.set_style(gtk
.TOOLBAR_BOTH_HORIZ
)
371 if hasattr(w
, 'set_label'):
373 self
.toolbar
.foreach(remove_label
)
374 # END TEMPORARY FIX FOR TOOLBAR STYLE
376 self
.app
.add_window(self
.window
)
377 self
.vMain
.reparent(self
.window
)
378 self
.gPodder
= self
.window
380 # Reparent the main menu
382 for child
in self
.mainMenu
.get_children():
384 self
.itemQuit
.reparent(menu
)
385 self
.window
.set_menu(menu
)
387 self
.mainMenu
.destroy()
390 # do some widget hiding
391 self
.toolbar
.remove(self
.toolTransfer
)
392 self
.itemTransferSelected
.hide_all()
393 self
.item_email_subscriptions
.hide_all()
395 # Feed cache update button
396 self
.label120
.set_text(_('Update'))
398 # get screen real estate
399 self
.hboxContainer
.set_border_width(0)
401 # Offer importing of videocenter podcasts
402 if os
.path
.exists(os
.path
.expanduser('~/videocenter')):
403 self
.item_upgrade_from_videocenter
.show()
404 self
.upgrade_from_videocenter_separator
.show()
406 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
407 self
.treeChannels
.connect('size-allocate', self
.on_tree_channels_resize
)
409 if gl
.config
.show_url_entry_in_podcast_list
:
410 self
.hboxAddChannel
.show()
412 if not gl
.config
.show_toolbar
:
415 gl
.config
.add_observer(self
.on_config_changed
)
416 self
.default_entry_text_color
= self
.entryAddChannel
.get_style().text
[gtk
.STATE_NORMAL
]
417 self
.entryAddChannel
.connect('focus-in-event', self
.entry_add_channel_focus
)
418 self
.entryAddChannel
.connect('focus-out-event', self
.entry_add_channel_unfocus
)
419 self
.entry_add_channel_unfocus(self
.entryAddChannel
, None)
422 self
.tray_icon
= None
423 self
.gpodder_episode_window
= None
425 self
.fullscreen
= False
426 self
.minimized
= False
427 self
.gPodder
.connect('window-state-event', self
.window_state_event
)
429 self
.already_notified_new_episodes
= []
430 self
.show_hide_tray_icon()
431 self
.no_episode_selected
.set_sensitive(False)
433 self
.itemShowToolbar
.set_active(gl
.config
.show_toolbar
)
434 self
.itemShowDescription
.set_active(gl
.config
.episode_list_descriptions
)
436 gl
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
437 gl
.config
.connect_gtk_paned( 'paned_position', self
.channelPaned
)
439 gl
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
440 gl
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
441 gl
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
442 gl
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
444 # Make sure we free/close the download queue when we
445 # update the "max downloads" spin button
446 changed_cb
= lambda spinbutton
: services
.download_status_manager
.update_max_downloads()
447 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
449 self
.default_title
= None
450 if app_version
.rfind('git') != -1:
451 self
.set_title('gPodder %s' % app_version
)
453 title
= self
.gPodder
.get_title()
454 if title
is not None:
455 self
.set_title(title
)
457 self
.set_title(_('gPodder'))
459 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
461 # cell renderers for channel tree
462 iconcolumn
= gtk
.TreeViewColumn('')
464 iconcell
= gtk
.CellRendererPixbuf()
465 iconcolumn
.pack_start( iconcell
, False)
466 iconcolumn
.add_attribute( iconcell
, 'pixbuf', 5)
467 self
.cell_channel_icon
= iconcell
469 namecolumn
= gtk
.TreeViewColumn('')
470 namecell
= gtk
.CellRendererText()
471 namecell
.set_property('foreground-set', True)
472 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
473 namecolumn
.pack_start( namecell
, True)
474 namecolumn
.add_attribute( namecell
, 'markup', 2)
475 namecolumn
.add_attribute( namecell
, 'foreground', 8)
477 iconcell
= gtk
.CellRendererPixbuf()
478 iconcell
.set_property('xalign', 1.0)
479 namecolumn
.pack_start( iconcell
, False)
480 namecolumn
.add_attribute( iconcell
, 'pixbuf', 3)
481 namecolumn
.add_attribute(iconcell
, 'visible', 7)
482 self
.cell_channel_pill
= iconcell
484 self
.treeChannels
.append_column(iconcolumn
)
485 self
.treeChannels
.append_column(namecolumn
)
486 self
.treeChannels
.set_headers_visible(False)
488 # enable alternating colors hint
489 self
.treeAvailable
.set_rules_hint( True)
490 self
.treeChannels
.set_rules_hint( True)
492 # connect to tooltip signals
494 self
.treeChannels
.set_property('has-tooltip', True)
495 self
.treeChannels
.connect('query-tooltip', self
.treeview_channels_query_tooltip
)
496 self
.treeAvailable
.set_property('has-tooltip', True)
497 self
.treeAvailable
.connect('query-tooltip', self
.treeview_episodes_query_tooltip
)
499 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender
= self
)
500 self
.last_tooltip_channel
= None
501 self
.last_tooltip_episode
= None
502 self
.podcast_list_can_tooltip
= True
503 self
.episode_list_can_tooltip
= True
505 # Add our context menu to treeAvailable
506 if gpodder
.interface
== gpodder
.MAEMO
:
507 self
.treeAvailable
.connect('button-release-event', self
.treeview_button_pressed
)
509 self
.treeAvailable
.connect('button-press-event', self
.treeview_button_pressed
)
510 self
.treeChannels
.connect('button-press-event', self
.treeview_channels_button_pressed
)
512 iconcell
= gtk
.CellRendererPixbuf()
513 if gpodder
.interface
== gpodder
.MAEMO
:
514 status_column_label
= ''
516 status_column_label
= _('Status')
517 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=4)
519 namecell
= gtk
.CellRendererText()
520 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
521 namecolumn
= gtk
.TreeViewColumn(_("Episode"), namecell
, markup
=6)
522 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
523 namecolumn
.set_expand(True)
525 sizecell
= gtk
.CellRendererText()
526 sizecolumn
= gtk
.TreeViewColumn( _("Size"), sizecell
, text
=2)
528 releasecell
= gtk
.CellRendererText()
529 releasecolumn
= gtk
.TreeViewColumn( _("Released"), releasecell
, text
=5)
531 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
532 itemcolumn
.set_reorderable(True)
533 self
.treeAvailable
.append_column(itemcolumn
)
535 if gpodder
.interface
== gpodder
.MAEMO
:
536 # Due to screen space contraints, we
537 # hide these columns here by default
538 self
.column_size
= sizecolumn
539 self
.column_released
= releasecolumn
540 self
.column_released
.set_visible(False)
541 self
.column_size
.set_visible(False)
543 # enable search in treeavailable
544 self
.treeAvailable
.set_search_equal_func( self
.treeAvailable_search_equal
)
546 # enable multiple selection support
547 self
.treeAvailable
.get_selection().set_mode( gtk
.SELECTION_MULTIPLE
)
548 self
.treeDownloads
.get_selection().set_mode( gtk
.SELECTION_MULTIPLE
)
550 # columns and renderers for "download progress" tab
551 episodecell
= gtk
.CellRendererText()
552 episodecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
553 episodecolumn
= gtk
.TreeViewColumn( _("Episode"), episodecell
, text
=0)
554 episodecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
555 episodecolumn
.set_expand(True)
557 speedcell
= gtk
.CellRendererText()
558 speedcolumn
= gtk
.TreeViewColumn( _("Speed"), speedcell
, text
=1)
560 progresscell
= gtk
.CellRendererProgress()
561 progresscolumn
= gtk
.TreeViewColumn( _("Progress"), progresscell
, value
=2)
562 progresscolumn
.set_expand(True)
564 for itemcolumn
in ( episodecolumn
, speedcolumn
, progresscolumn
):
565 self
.treeDownloads
.append_column( itemcolumn
)
567 # After we've set up most of the window, show it :)
568 if not gpodder
.interface
== gpodder
.MAEMO
:
572 if gl
.config
.start_iconified
:
573 self
.iconify_main_window()
574 elif gl
.config
.minimize_to_tray
:
575 self
.tray_icon
.set_visible(False)
577 # a dictionary that maps episode URLs to the current
578 # treeAvailable row numbers to generate tree paths
579 self
.url_path_mapping
= {}
581 # a dictionary that maps channel URLs to the current
582 # treeChannels row numbers to generate tree paths
583 self
.channel_url_path_mapping
= {}
585 services
.download_status_manager
.register( 'list-changed', self
.download_status_updated
)
586 services
.download_status_manager
.register( 'progress-changed', self
.download_progress_updated
)
587 services
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
588 services
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
589 self
.cover_cache
= {}
591 self
.treeDownloads
.set_model( services
.download_status_manager
.tree_model
)
593 #Add Drag and Drop Support
594 flags
= gtk
.DEST_DEFAULT_ALL
595 targets
= [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
596 actions
= gtk
.gdk
.ACTION_DEFAULT | gtk
.gdk
.ACTION_COPY
597 self
.treeChannels
.drag_dest_set( flags
, targets
, actions
)
598 self
.treeChannels
.connect( 'drag_data_received', self
.drag_data_received
)
600 # Subscribed channels
601 self
.active_channel
= None
602 self
.channels
= load_channels()
603 self
.channel_list_changed
= True
604 self
.update_podcasts_tab()
606 # load list of user applications for audio playback
607 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
608 Thread(target
=self
.read_apps
).start()
610 # Clean up old, orphaned download files
611 gl
.clean_up_downloads( delete_partial
= True)
613 # Set the "Device" menu item for the first time
614 self
.update_item_device()
616 # Last folder used for saving episodes
617 self
.folder_for_saving_episodes
= None
619 # Set up default channel colors
620 self
.channel_colors
= {
622 'updating': gl
.config
.color_updating_feeds
,
623 'parse_error': '#ff0000',
626 # Now, update the feed cache, when everything's in place
627 self
.btnUpdateFeeds
.show_all()
628 self
.updated_feeds
= 0
629 self
.updating_feed_cache
= False
630 self
.feed_cache_update_cancelled
= False
631 self
.update_feed_cache(force_update
=gl
.config
.update_on_startup
)
633 # Start the auto-update procedure
634 self
.auto_update_procedure(first_run
=True)
636 # Delete old episodes if the user wishes to
637 if gl
.config
.auto_remove_old_episodes
:
638 old_episodes
= self
.get_old_episodes()
639 if len(old_episodes
) > 0:
640 self
.delete_episode_list(old_episodes
, confirm
=False)
641 self
.updateComboBox()
643 # First-time users should be asked if they want to see the OPML
644 if len(self
.channels
) == 0:
645 util
.idle_add(self
.on_itemUpdate_activate
, None)
647 def on_tree_channels_resize(self
, widget
, allocation
):
648 if not gl
.config
.podcast_sidebar_save_space
:
651 window_allocation
= self
.gPodder
.get_allocation()
652 percentage
= 100. * float(allocation
.width
) / float(window_allocation
.width
)
653 if hasattr(self
, 'cell_channel_icon'):
654 self
.cell_channel_icon
.set_property('visible', bool(percentage
> 22.))
655 if hasattr(self
, 'cell_channel_pill'):
656 self
.cell_channel_pill
.set_property('visible', bool(percentage
> 25.))
658 def entry_add_channel_focus(self
, widget
, event
):
659 widget
.modify_text(gtk
.STATE_NORMAL
, self
.default_entry_text_color
)
660 if widget
.get_text() == self
.ENTER_URL_TEXT
:
663 def entry_add_channel_unfocus(self
, widget
, event
):
664 if widget
.get_text() == '':
665 widget
.set_text(self
.ENTER_URL_TEXT
)
666 widget
.modify_text(gtk
.STATE_NORMAL
, gtk
.gdk
.color_parse('#aaaaaa'))
668 def on_config_changed(self
, name
, old_value
, new_value
):
669 if name
== 'show_toolbar':
674 elif name
== 'episode_list_descriptions':
675 self
.updateTreeView()
676 elif name
== 'show_url_entry_in_podcast_list':
678 self
.hboxAddChannel
.show()
680 self
.hboxAddChannel
.hide()
683 time
.sleep(3) # give other parts of gpodder a chance to start up
684 self
.user_apps_reader
.read()
685 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
686 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
688 def treeview_episodes_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
689 # With get_bin_window, we get the window that contains the rows without
690 # the header. The Y coordinate of this window will be the height of the
691 # treeview header. This is the amount we have to subtract from the
692 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
693 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
696 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
698 if not self
.episode_list_can_tooltip
or (column
is not None and column
!= treeview
.get_columns()[0]):
699 self
.last_tooltip_episode
= None
703 model
= treeview
.get_model()
704 iter = model
.get_iter(path
)
705 url
= model
.get_value(iter, 0)
706 description
= model
.get_value(iter, 7)
707 if self
.last_tooltip_episode
is not None and self
.last_tooltip_episode
!= url
:
708 self
.last_tooltip_episode
= None
710 self
.last_tooltip_episode
= url
712 if len(description
) > 400:
713 description
= description
[:398]+'[...]'
715 tooltip
.set_text(description
)
718 self
.last_tooltip_episode
= None
721 def podcast_list_allow_tooltips(self
):
722 self
.podcast_list_can_tooltip
= True
724 def episode_list_allow_tooltips(self
):
725 self
.episode_list_can_tooltip
= True
727 def treeview_channels_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
728 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
730 if not self
.podcast_list_can_tooltip
or (column
is not None and column
!= treeview
.get_columns()[0]):
731 self
.last_tooltip_channel
= None
735 model
= treeview
.get_model()
736 iter = model
.get_iter(path
)
737 url
= model
.get_value(iter, 0)
738 for channel
in self
.channels
:
739 if channel
.url
== url
:
740 if self
.last_tooltip_channel
is not None and self
.last_tooltip_channel
!= channel
:
741 self
.last_tooltip_channel
= None
743 self
.last_tooltip_channel
= channel
744 channel
.request_save_dir_size()
745 diskspace_str
= gl
.format_filesize(channel
.save_dir_size
, 0)
746 error_str
= model
.get_value(iter, 6)
748 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
749 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
750 table
= gtk
.Table(rows
=3, columns
=3)
751 table
.set_row_spacings(5)
752 table
.set_col_spacings(5)
753 table
.set_border_width(5)
755 heading
= gtk
.Label()
756 heading
.set_alignment(0, 1)
757 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
758 table
.attach(heading
, 0, 1, 0, 1)
759 size_info
= gtk
.Label()
760 size_info
.set_alignment(1, 1)
761 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
762 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
763 table
.attach(size_info
, 2, 3, 0, 1)
765 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
767 if len(channel
.description
) < 500:
768 description
= channel
.description
770 pos
= channel
.description
.find('\n\n')
771 if pos
== -1 or pos
> 500:
772 description
= channel
.description
[:498]+'[...]'
774 description
= channel
.description
[:pos
]
776 description
= gtk
.Label(description
)
778 description
.set_markup(error_str
)
779 description
.set_alignment(0, 0)
780 description
.set_line_wrap(True)
781 table
.attach(description
, 0, 3, 2, 3)
784 tooltip
.set_custom(table
)
788 self
.last_tooltip_channel
= None
791 def update_m3u_playlist_clicked(self
, widget
):
792 self
.active_channel
.update_m3u_playlist()
793 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
795 def treeview_channels_button_pressed( self
, treeview
, event
):
796 global WEB_BROWSER_ICON
798 if event
.button
== 3:
799 ( x
, y
) = ( int(event
.x
), int(event
.y
) )
800 ( path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
804 # Did the user right-click into a selection?
805 selection
= treeview
.get_selection()
806 if selection
.count_selected_rows() and path
:
807 ( model
, paths
) = selection
.get_selected_rows()
808 if path
not in paths
:
809 # We have right-clicked, but not into the
810 # selection, assume we don't want to operate
814 # No selection or right click not in selection:
815 # Select the single item where we clicked
816 if not len( paths
) and path
:
817 treeview
.grab_focus()
818 treeview
.set_cursor( path
, column
, 0)
820 ( model
, paths
) = ( treeview
.get_model(), [ path
] )
822 # We did not find a selection, and the user didn't
823 # click on an item to select -- don't show the menu
829 item
= gtk
.ImageMenuItem( _('Open download folder'))
830 item
.set_image( gtk
.image_new_from_icon_name( 'folder-open', gtk
.ICON_SIZE_MENU
))
831 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
834 item
= gtk
.ImageMenuItem( _('Update Feed'))
835 item
.set_image( gtk
.image_new_from_icon_name( 'gtk-refresh', gtk
.ICON_SIZE_MENU
))
836 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
837 item
.set_sensitive( not self
.updating_feed_cache
)
840 if gl
.config
.create_m3u_playlists
:
841 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
842 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
843 item
.connect('activate', self
.update_m3u_playlist_clicked
)
846 if self
.active_channel
.link
:
847 item
= gtk
.ImageMenuItem(_('Visit website'))
848 item
.set_image(gtk
.image_new_from_icon_name(WEB_BROWSER_ICON
, gtk
.ICON_SIZE_MENU
))
849 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
852 if self
.active_channel
.channel_is_locked
:
853 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
854 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
855 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
856 menu
.append(self
.set_finger_friendly(item
))
858 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
859 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
860 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
861 menu
.append(self
.set_finger_friendly(item
))
864 menu
.append( gtk
.SeparatorMenuItem())
866 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
867 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
870 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
871 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
875 # Disable tooltips while we are showing the menu, so
876 # the tooltip will not appear over the menu
877 self
.podcast_list_can_tooltip
= False
878 menu
.connect('deactivate', lambda menushell
: self
.podcast_list_allow_tooltips())
879 menu
.popup( None, None, None, event
.button
, event
.time
)
883 def on_itemClose_activate(self
, widget
):
884 if self
.tray_icon
is not None:
885 if gpodder
.interface
== gpodder
.MAEMO
:
886 self
.gPodder
.set_property('visible', False)
888 self
.iconify_main_window()
890 self
.on_gPodder_delete_event(widget
)
892 def cover_file_removed(self
, channel_url
):
894 The Cover Downloader calls this when a previously-
895 available cover has been removed from the disk. We
896 have to update our cache to reflect this change.
898 (COLUMN_URL
, COLUMN_PIXBUF
) = (0, 5)
899 for row
in self
.treeChannels
.get_model():
900 if row
[COLUMN_URL
] == channel_url
:
901 row
[COLUMN_PIXBUF
] = None
902 key
= (channel_url
, gl
.config
.podcast_list_icon_size
, \
903 gl
.config
.podcast_list_icon_size
)
904 if key
in self
.cover_cache
:
905 del self
.cover_cache
[key
]
908 def cover_download_finished(self
, channel_url
, pixbuf
):
910 The Cover Downloader calls this when it has finished
911 downloading (or registering, if already downloaded)
912 a new channel cover, which is ready for displaying.
914 if pixbuf
is not None:
915 (COLUMN_URL
, COLUMN_PIXBUF
) = (0, 5)
916 for row
in self
.treeChannels
.get_model():
917 if row
[COLUMN_URL
] == channel_url
and row
[COLUMN_PIXBUF
] is None:
918 new_pixbuf
= util
.resize_pixbuf_keep_ratio(pixbuf
, gl
.config
.podcast_list_icon_size
, gl
.config
.podcast_list_icon_size
, channel_url
, self
.cover_cache
)
919 row
[COLUMN_PIXBUF
] = new_pixbuf
or pixbuf
921 def save_episode_as_file( self
, url
, *args
):
922 episode
= self
.active_channel
.find_episode(url
)
924 folder
= self
.folder_for_saving_episodes
925 (result
, folder
) = self
.show_copy_dialog(src_filename
=episode
.local_filename(), dst_filename
=episode
.sync_filename(), dst_directory
=folder
)
926 self
.folder_for_saving_episodes
= folder
928 def copy_episode_bluetooth(self
, url
, *args
):
929 episode
= self
.active_channel
.find_episode(url
)
930 filename
= episode
.local_filename()
932 if gl
.config
.bluetooth_use_device_address
:
933 device
= gl
.config
.bluetooth_device_address
937 destfile
= os
.path
.join(gl
.tempdir
, util
.sanitize_filename(episode
.sync_filename()))
938 (base
, ext
) = os
.path
.splitext(filename
)
939 if not destfile
.endswith(ext
):
942 if gl
.config
.bluetooth_use_converter
:
943 title
= _('Converting file')
944 message
= _('Please wait while gPodder converts your media file for bluetooth file transfer.')
945 dlg
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_NONE
)
947 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
952 def convert_and_send_thread(filename
, destfile
, device
, dialog
, notify
):
953 if gl
.config
.bluetooth_use_converter
:
954 p
= subprocess
.Popen([gl
.config
.bluetooth_converter
, filename
, destfile
], stdout
=sys
.stdout
, stderr
=sys
.stderr
)
956 if dialog
is not None:
960 shutil
.copyfile(filename
, destfile
)
963 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
966 if result
== 0 or not os
.path
.exists(destfile
):
967 util
.bluetooth_send_file(destfile
, device
)
969 notify(_('Error converting file.'), _('Bluetooth file transfer'))
970 util
.delete_file(destfile
)
972 Thread(target
=convert_and_send_thread
, args
=[filename
, destfile
, device
, dlg
, self
.notification
]).start()
974 def treeview_button_pressed( self
, treeview
, event
):
975 global WEB_BROWSER_ICON
977 # Use right-click for the Desktop version and left-click for Maemo
978 if (event
.button
== 1 and gpodder
.interface
== gpodder
.MAEMO
) or \
979 (event
.button
== 3 and gpodder
.interface
== gpodder
.GUI
):
980 ( x
, y
) = ( int(event
.x
), int(event
.y
) )
981 ( path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
985 # Did the user right-click into a selection?
986 selection
= self
.treeAvailable
.get_selection()
987 if selection
.count_selected_rows() and path
:
988 ( model
, paths
) = selection
.get_selected_rows()
989 if path
not in paths
:
990 # We have right-clicked, but not into the
991 # selection, assume we don't want to operate
995 # No selection or right click not in selection:
996 # Select the single item where we clicked
997 if not len( paths
) and path
:
998 treeview
.grab_focus()
999 treeview
.set_cursor( path
, column
, 0)
1001 ( model
, paths
) = ( treeview
.get_model(), [ path
] )
1003 # We did not find a selection, and the user didn't
1004 # click on an item to select -- don't show the menu
1008 first_url
= model
.get_value( model
.get_iter( paths
[0]), 0)
1009 episode
= db
.load_episode(first_url
)
1013 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1016 if open_instead_of_play
:
1017 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1019 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1020 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.toolPlay
))
1021 menu
.append(self
.set_finger_friendly(item
))
1023 if not episode
['is_locked'] and can_delete
:
1024 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1025 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1026 menu
.append(self
.set_finger_friendly(item
))
1029 item
= gtk
.ImageMenuItem( _('Cancel download'))
1030 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_STOP
, gtk
.ICON_SIZE_MENU
))
1031 item
.connect( 'activate', lambda w
: self
.on_treeDownloads_row_activated( self
.toolCancel
))
1032 menu
.append(self
.set_finger_friendly(item
))
1035 item
= gtk
.ImageMenuItem(_('Download'))
1036 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1037 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.toolDownload
))
1038 menu
.append(self
.set_finger_friendly(item
))
1040 if episode
['state'] == db
.STATE_NORMAL
and not episode
['is_played']: # can_download:
1041 item
= gtk
.ImageMenuItem(_('Do not download'))
1042 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1043 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1044 menu
.append(self
.set_finger_friendly(item
))
1045 elif episode
['state'] == db
.STATE_NORMAL
and can_download
:
1046 item
= gtk
.ImageMenuItem(_('Mark as new'))
1047 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
1048 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1049 menu
.append(self
.set_finger_friendly(item
))
1051 if can_play
and not can_download
:
1052 menu
.append( gtk
.SeparatorMenuItem())
1053 item
= gtk
.ImageMenuItem(_('Save to disk'))
1054 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
1055 item
.connect( 'activate', lambda w
: self
.for_each_selected_episode_url(self
.save_episode_as_file
))
1056 menu
.append(self
.set_finger_friendly(item
))
1057 if gl
.bluetooth_available
:
1058 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
1059 item
.set_image(gtk
.image_new_from_icon_name('bluetooth', gtk
.ICON_SIZE_MENU
))
1060 item
.connect('activate', lambda w
: self
.copy_episode_bluetooth(episode_url
))
1061 menu
.append(self
.set_finger_friendly(item
))
1063 item
= gtk
.ImageMenuItem(_('Transfer to %s') % gl
.get_device_name())
1064 item
.set_image(gtk
.image_new_from_icon_name('multimedia-player', gtk
.ICON_SIZE_MENU
))
1065 item
.connect('activate', lambda w
: self
.on_treeAvailable_row_activated(self
.toolTransfer
))
1066 menu
.append(self
.set_finger_friendly(item
))
1069 menu
.append( gtk
.SeparatorMenuItem())
1070 is_played
= episode
['is_played']
1072 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
1073 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
1074 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
1075 menu
.append(self
.set_finger_friendly(item
))
1077 item
= gtk
.ImageMenuItem(_('Mark as played'))
1078 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
1079 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
1080 menu
.append(self
.set_finger_friendly(item
))
1082 is_locked
= episode
['is_locked']
1084 item
= gtk
.ImageMenuItem(_('Allow deletion'))
1085 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1086 item
.connect('activate', self
.on_item_toggle_lock_activate
)
1087 menu
.append(self
.set_finger_friendly(item
))
1089 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
1090 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1091 item
.connect('activate', self
.on_item_toggle_lock_activate
)
1092 menu
.append(self
.set_finger_friendly(item
))
1095 menu
.append(gtk
.SeparatorMenuItem())
1096 # Single item, add episode information menu item
1097 episode_url
= model
.get_value( model
.get_iter( paths
[0]), 0)
1098 item
= gtk
.ImageMenuItem(_('Episode details'))
1099 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1100 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.treeAvailable
))
1101 menu
.append(self
.set_finger_friendly(item
))
1102 episode
= self
.active_channel
.find_episode(episode_url
)
1103 # If we have it, also add episode website link
1104 if episode
and episode
.link
and episode
.link
!= episode
.url
:
1105 item
= gtk
.ImageMenuItem(_('Visit website'))
1106 item
.set_image(gtk
.image_new_from_icon_name(WEB_BROWSER_ICON
, gtk
.ICON_SIZE_MENU
))
1107 item
.connect('activate', lambda w
: util
.open_website(episode
.link
))
1108 menu
.append(self
.set_finger_friendly(item
))
1110 if gpodder
.interface
== gpodder
.MAEMO
:
1111 # Because we open the popup on left-click for Maemo,
1112 # we also include a non-action to close the menu
1113 menu
.append(gtk
.SeparatorMenuItem())
1114 item
= gtk
.ImageMenuItem(_('Close this menu'))
1115 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1116 menu
.append(self
.set_finger_friendly(item
))
1119 # Disable tooltips while we are showing the menu, so
1120 # the tooltip will not appear over the menu
1121 self
.episode_list_can_tooltip
= False
1122 menu
.connect('deactivate', lambda menushell
: self
.episode_list_allow_tooltips())
1123 menu
.popup( None, None, None, event
.button
, event
.time
)
1127 def set_title(self
, new_title
):
1128 self
.default_title
= new_title
1129 self
.gPodder
.set_title(new_title
)
1131 def download_progress_updated( self
, count
, percentage
):
1132 title
= [ self
.default_title
]
1134 total_speed
= gl
.format_filesize(services
.download_status_manager
.total_speed())
1137 title
.append( _('downloading one file'))
1139 title
.append( _('downloading %d files') % count
)
1142 title
[1] = ''.join( [ title
[1], ' (%d%%, %s/s)' % (percentage
, total_speed
) ])
1144 self
.gPodder
.set_title( ' - '.join( title
))
1146 # Have all the downloads completed?
1147 # If so execute user command if defined, else do nothing
1149 if len(gl
.config
.cmd_all_downloads_complete
) > 0:
1150 Thread(target
=gl
.ext_command_thread
, args
=(self
.notification
,gl
.config
.cmd_all_downloads_complete
)).start()
1152 def update_selected_episode_list_icons(self
):
1154 Updates the status icons in the episode list
1156 selection
= self
.treeAvailable
.get_selection()
1157 (model
, paths
) = selection
.get_selected_rows()
1159 iter = model
.get_iter(path
)
1160 self
.active_channel
.iter_set_downloading_columns(model
, iter)
1162 def update_episode_list_icons(self
, urls
):
1164 Updates the status icons in the episode list
1165 Only update the episodes that have an URL in
1166 the "urls" iterable object (e.g. a list of URLs)
1168 if self
.active_channel
is None:
1171 model
= self
.treeAvailable
.get_model()
1176 if url
in self
.url_path_mapping
:
1177 path
= (self
.url_path_mapping
[url
],)
1178 self
.active_channel
.iter_set_downloading_columns(model
, model
.get_iter(path
))
1180 def playback_episode(self
, episode
, stream
=False):
1181 (success
, application
) = gl
.playback_episode(episode
, stream
)
1183 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
), ))
1184 self
.update_selected_episode_list_icons()
1185 self
.updateComboBox(only_selected_channel
=True)
1187 def treeAvailable_search_equal( self
, model
, column
, key
, iter, data
= None):
1193 # columns, as defined in libpodcasts' get model method
1194 # 1 = episode title, 7 = description
1197 for column
in columns
:
1198 value
= model
.get_value( iter, column
).lower()
1199 if value
.find( key
) != -1:
1204 def change_menu_item(self
, menuitem
, icon
=None, label
=None):
1205 if icon
is not None:
1206 menuitem
.get_image().set_from_icon_name(icon
, gtk
.ICON_SIZE_MENU
)
1207 if label
is not None:
1208 label_widget
= menuitem
.get_child()
1209 label_widget
.set_text(label
)
1211 def play_or_download(self
):
1212 if self
.wNotebook
.get_current_page() > 0:
1215 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1216 ( is_played
, is_locked
) = (False,)*2
1218 open_instead_of_play
= False
1220 selection
= self
.treeAvailable
.get_selection()
1221 if selection
.count_selected_rows() > 0:
1222 (model
, paths
) = selection
.get_selected_rows()
1225 url
= model
.get_value( model
.get_iter( path
), 0)
1226 local_filename
= model
.get_value( model
.get_iter( path
), 8)
1228 episode
= podcastItem
.load(url
, self
.active_channel
)
1230 if episode
.file_type() not in ('audio', 'video'):
1231 open_instead_of_play
= True
1233 if episode
.was_downloaded():
1234 can_play
= episode
.was_downloaded(and_exists
=True)
1236 is_played
= episode
.is_played
1237 is_locked
= episode
.is_locked
1241 if services
.download_status_manager
.is_download_in_progress(url
):
1246 can_download
= can_download
and not can_cancel
1247 can_play
= gl
.config
.enable_streaming
or (can_play
and not can_cancel
and not can_download
)
1248 can_transfer
= can_play
and gl
.config
.device_type
!= 'none' and not can_cancel
and not can_download
1250 if open_instead_of_play
:
1251 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
1252 can_transfer
= False
1254 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
1256 self
.toolPlay
.set_sensitive( can_play
)
1257 self
.toolDownload
.set_sensitive( can_download
)
1258 self
.toolTransfer
.set_sensitive( can_transfer
)
1259 self
.toolCancel
.set_sensitive( can_cancel
)
1262 self
.item_cancel_download
.show_all()
1264 self
.item_cancel_download
.hide_all()
1266 self
.itemDownloadSelected
.show_all()
1268 self
.itemDownloadSelected
.hide_all()
1270 if open_instead_of_play
:
1271 self
.itemOpenSelected
.show_all()
1272 self
.itemPlaySelected
.hide_all()
1274 self
.itemPlaySelected
.show_all()
1275 self
.itemOpenSelected
.hide_all()
1276 if not can_download
:
1277 self
.itemDeleteSelected
.show_all()
1279 self
.itemDeleteSelected
.hide_all()
1280 self
.item_toggle_played
.show_all()
1281 self
.item_toggle_lock
.show_all()
1282 self
.separator9
.show_all()
1284 self
.change_menu_item(self
.item_toggle_played
, gtk
.STOCK_CANCEL
, _('Mark as unplayed'))
1286 self
.change_menu_item(self
.item_toggle_played
, gtk
.STOCK_APPLY
, _('Mark as played'))
1288 self
.change_menu_item(self
.item_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Allow deletion'))
1290 self
.change_menu_item(self
.item_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Prohibit deletion'))
1292 self
.itemPlaySelected
.hide_all()
1293 self
.itemOpenSelected
.hide_all()
1294 self
.itemDeleteSelected
.hide_all()
1295 self
.item_toggle_played
.hide_all()
1296 self
.item_toggle_lock
.hide_all()
1297 self
.separator9
.hide_all()
1298 if can_play
or can_download
or can_cancel
:
1299 self
.item_episode_details
.show_all()
1300 self
.separator16
.show_all()
1301 self
.no_episode_selected
.hide_all()
1303 self
.item_episode_details
.hide_all()
1304 self
.separator16
.hide_all()
1305 self
.no_episode_selected
.show_all()
1307 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
1309 def download_status_updated(self
, episode_urls
, channel_urls
):
1310 count
= services
.download_status_manager
.count()
1312 self
.labelDownloads
.set_text( _('Downloads (%d)') % count
)
1314 self
.labelDownloads
.set_text( _('Downloads'))
1316 self
.update_episode_list_icons(episode_urls
)
1317 self
.updateComboBox(only_these_urls
=channel_urls
)
1319 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1320 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1322 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1323 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1325 def episode_new_status_changed(self
, urls
):
1326 self
.updateComboBox()
1327 self
.update_episode_list_icons(urls
)
1329 def updateComboBox(self
, selected_url
=None, only_selected_channel
=False, only_these_urls
=None):
1330 selection
= self
.treeChannels
.get_selection()
1331 (model
, iter) = selection
.get_selected()
1333 if only_selected_channel
:
1334 # very cheap! only update selected channel
1335 if iter and self
.active_channel
is not None:
1336 update_channel_model_by_iter(model
, iter,
1337 self
.active_channel
, self
.channel_colors
,
1339 gl
.config
.podcast_list_icon_size
,
1340 gl
.config
.podcast_list_icon_size
)
1341 elif not self
.channel_list_changed
:
1342 # we can keep the model, but have to update some
1343 if only_these_urls
is None:
1344 # still cheaper than reloading the whole list
1345 iter = model
.get_iter_first()
1346 while iter is not None:
1347 (index
,) = model
.get_path(iter)
1348 update_channel_model_by_iter(model
, iter,
1349 self
.channels
[index
], self
.channel_colors
,
1351 gl
.config
.podcast_list_icon_size
,
1352 gl
.config
.podcast_list_icon_size
)
1353 iter = model
.iter_next(iter)
1355 # ok, we got a bunch of urls to update
1356 for url
in only_these_urls
:
1357 if url
in self
.channel_url_path_mapping
:
1358 index
= self
.channel_url_path_mapping
[url
]
1360 iter = model
.get_iter(path
)
1361 update_channel_model_by_iter(model
, iter,
1362 self
.channels
[index
], self
.channel_colors
,
1364 gl
.config
.podcast_list_icon_size
,
1365 gl
.config
.podcast_list_icon_size
)
1367 if model
and iter and selected_url
is None:
1368 # Get the URL of the currently-selected podcast
1369 selected_url
= model
.get_value(iter, 0)
1371 (model
, urls
) = channels_to_model(self
.channels
,
1372 self
.channel_colors
, self
.cover_cache
,
1373 gl
.config
.podcast_list_icon_size
,
1374 gl
.config
.podcast_list_icon_size
)
1376 self
.channel_url_path_mapping
= dict(zip(urls
, range(len(urls
))))
1377 self
.treeChannels
.set_model(model
)
1380 selected_path
= (0,)
1381 # Find the previously-selected URL in the new
1382 # model if we have an URL (else select first)
1383 if selected_url
is not None:
1384 pos
= model
.get_iter_first()
1385 while pos
is not None:
1386 url
= model
.get_value(pos
, 0)
1387 if url
== selected_url
:
1388 selected_path
= model
.get_path(pos
)
1390 pos
= model
.iter_next(pos
)
1392 self
.treeChannels
.get_selection().select_path(selected_path
)
1394 log( 'Cannot set selection on treeChannels', sender
= self
)
1395 self
.on_treeChannels_cursor_changed( self
.treeChannels
)
1396 self
.channel_list_changed
= False
1398 def updateTreeView(self
):
1399 if self
.channels
and self
.active_channel
is not None:
1400 (model
, urls
) = self
.active_channel
.get_tree_model()
1401 self
.treeAvailable
.set_model(model
)
1402 self
.url_path_mapping
= dict(zip(urls
, range(len(urls
))))
1403 self
.treeAvailable
.columns_autosize()
1404 self
.play_or_download()
1406 model
= self
.treeAvailable
.get_model()
1407 if model
is not None:
1410 def drag_data_received(self
, widget
, context
, x
, y
, sel
, ttype
, time
):
1411 (path
, column
, rx
, ry
) = self
.treeChannels
.get_path_at_pos( x
, y
) or (None,)*4
1414 if path
is not None:
1415 model
= self
.treeChannels
.get_model()
1416 iter = model
.get_iter(path
)
1417 url
= model
.get_value(iter, 0)
1418 for channel
in self
.channels
:
1419 if channel
.url
== url
:
1420 dnd_channel
= channel
1424 rl
= result
.strip().lower()
1425 if (rl
.endswith('.jpg') or rl
.endswith('.png') or rl
.endswith('.gif') or rl
.endswith('.svg')) and dnd_channel
is not None:
1426 services
.cover_downloader
.replace_cover(dnd_channel
, result
)
1428 self
.add_new_channel(result
)
1430 def add_new_channel(self
, result
=None, ask_download_new
=True, quiet
=False, block
=False, authentication_tokens
=None):
1431 (scheme
, rest
) = result
.split('://', 1)
1432 result
= util
.normalize_feed_url(result
)
1435 cute_scheme
= saxutils
.escape(scheme
)+'://'
1436 title
= _('%s URLs are not supported') % cute_scheme
1437 message
= _('gPodder does not understand the URL you supplied.')
1438 self
.show_message( message
, title
)
1441 for old_channel
in self
.channels
:
1442 if old_channel
.url
== result
:
1443 log( 'Channel already exists: %s', result
)
1444 # Select the existing channel in combo box
1445 for i
in range( len( self
.channels
)):
1446 if self
.channels
[i
] == old_channel
:
1447 self
.treeChannels
.get_selection().select_path( (i
,))
1448 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1450 self
.show_message( _('You have already subscribed to this podcast: %s') % (
1451 saxutils
.escape( old_channel
.title
), ), _('Already added'))
1454 waitdlg
= gtk
.MessageDialog(self
.gPodder
, 0, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_NONE
)
1455 waitdlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1456 waitdlg
.set_title(_('Downloading episode list'))
1457 waitdlg
.set_markup('<b><big>%s</big></b>' % waitdlg
.get_title())
1458 waitdlg
.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result
)
1459 waitpb
= gtk
.ProgressBar()
1461 waitdlg
.vbox
.add(waitpb
)
1463 waitdlg
.set_response_sensitive(gtk
.RESPONSE_CANCEL
, False)
1465 self
.entryAddChannel
.set_text(_('Downloading feed...'))
1466 self
.entryAddChannel
.set_sensitive(False)
1467 self
.btnAddChannel
.set_sensitive(False)
1468 args
= (result
, self
.add_new_channel_finish
, authentication_tokens
, ask_download_new
, quiet
, waitdlg
)
1469 thread
= Thread( target
=self
.add_new_channel_proc
, args
=args
)
1472 while block
and thread
.isAlive():
1473 while gtk
.events_pending():
1474 gtk
.main_iteration( False)
1479 def add_new_channel_proc( self
, url
, callback
, authentication_tokens
, *callback_args
):
1480 log( 'Adding new channel: %s', url
)
1481 channel
= error
= None
1483 channel
= podcastChannel
.load(url
=url
, create
=True, authentication_tokens
=authentication_tokens
)
1484 except HTTPAuthError
, e
:
1486 except Exception, e
:
1487 log('Error in podcastChannel.load(%s): %s', url
, e
, traceback
=True, sender
=self
)
1489 util
.idle_add( callback
, channel
, url
, error
, *callback_args
)
1491 def add_new_channel_finish( self
, channel
, url
, error
, ask_download_new
, quiet
, waitdlg
):
1492 if channel
is not None:
1493 self
.channels
.append( channel
)
1494 self
.channel_list_changed
= True
1495 save_channels( self
.channels
)
1497 # download changed channels and select the new episode in the UI afterwards
1498 self
.update_feed_cache(force_update
=False, select_url_afterwards
=channel
.url
)
1500 (username
, password
) = util
.username_password_from_url( url
)
1501 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')):
1502 channel
.username
= username
1503 channel
.password
= password
1504 log('Saving authentication data for episode downloads..', sender
= self
)
1506 # We need to update the channel list otherwise the authentication
1507 # data won't show up in the channel editor.
1508 # TODO: Only updated the newly added feed to save some cpu cycles
1509 self
.channels
= load_channels()
1510 self
.channel_list_changed
= True
1512 if ask_download_new
:
1513 new_episodes
= channel
.get_new_episodes()
1514 if len(new_episodes
):
1515 self
.new_episodes_show(new_episodes
)
1517 elif isinstance( error
, HTTPAuthError
):
1518 response
, auth_tokens
= self
.UsernamePasswordDialog(
1519 _('Feed requires authentication'), _('Please enter your username and password.'))
1522 self
.add_new_channel( url
, authentication_tokens
=auth_tokens
)
1525 # Ok, the URL is not a channel, or there is some other
1526 # error - let's see if it's a web page or OPML file...
1528 data
= urllib2
.urlopen(url
).read().lower()
1529 if '</opml>' in data
:
1530 # This looks like an OPML feed
1531 self
.on_item_import_from_file_activate(None, url
)
1533 elif '</html>' in data
:
1534 # This looks like a web page
1535 title
= _('The URL is a website')
1536 message
= _('The URL you specified points to a web page. You need to find the "feed" URL of the podcast to add to gPodder. Do you want to visit this website now and look for the podcast feed URL?\n\n(Hint: Look for "XML feed", "RSS feed" or "Podcast feed" if you are unsure for what to look. If there is only an iTunes URL, try adding this one.)')
1537 if self
.show_confirmation(message
, title
):
1538 util
.open_website(url
)
1540 except Exception, e
:
1541 log('Error trying to handle the URL as OPML or web page: %s', e
, sender
=self
)
1543 title
= _('Error adding podcast')
1544 message
= _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1545 self
.show_message( message
, title
)
1547 self
.entryAddChannel
.set_text(self
.ENTER_URL_TEXT
)
1548 self
.entryAddChannel
.set_sensitive(True)
1549 self
.btnAddChannel
.set_sensitive(True)
1550 self
.update_podcasts_tab()
1554 def update_feed_cache_finish_callback(self
, channels
=None,
1555 notify_no_new_episodes
=False, select_url_afterwards
=None):
1559 self
.updating_feed_cache
= False
1560 self
.hboxUpdateFeeds
.hide_all()
1561 self
.btnUpdateFeeds
.show_all()
1562 self
.itemUpdate
.set_sensitive(True)
1563 self
.itemUpdateChannel
.set_sensitive(True)
1565 # If we want to select a specific podcast (via its URL)
1566 # after the update, we give it to updateComboBox here to
1567 # select exactly this podcast after updating the view
1568 self
.updateComboBox(selected_url
=select_url_afterwards
)
1571 self
.tray_icon
.set_status(None)
1574 # look for new episodes to notify
1575 for channel
in self
.channels
:
1576 for episode
in channel
.get_new_episodes():
1577 if not episode
in self
.already_notified_new_episodes
:
1578 new_episodes
.append(episode
)
1579 self
.already_notified_new_episodes
.append(episode
)
1580 # notify new episodes
1582 if len(new_episodes
) == 0:
1583 if notify_no_new_episodes
and self
.tray_icon
is not None:
1584 msg
= _('No new episodes available for download')
1585 self
.tray_icon
.send_notification(msg
)
1587 elif len(new_episodes
) == 1:
1588 title
= _('gPodder has found %s') % (_('one new episode:'),)
1590 title
= _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes
))
1591 message
= self
.tray_icon
.format_episode_list(new_episodes
)
1593 #auto download new episodes
1594 if gl
.config
.auto_download_when_minimized
:
1595 message
+= '\n<i>(%s...)</i>' % _('downloading')
1596 self
.download_episode_list(new_episodes
)
1597 self
.tray_icon
.send_notification(message
, title
)
1600 # open the episodes selection dialog
1601 self
.channels
= load_channels()
1602 self
.channel_list_changed
= True
1603 self
.updateComboBox()
1604 if not self
.feed_cache_update_cancelled
:
1605 self
.download_all_new(channels
=channels
)
1607 def update_feed_cache_callback(self
, progressbar
, title
, position
, count
):
1608 progression
= _('Updated %s (%d/%d)')%(title
, position
+1, count
)
1609 progressbar
.set_text(progression
)
1611 self
.tray_icon
.set_status(
1612 self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
1614 progressbar
.set_fraction(float(position
)/float(count
))
1616 def update_feed_cache_proc( self
, channel
, total_channels
, semaphore
,
1617 callback_proc
, finish_proc
):
1620 if not self
.feed_cache_update_cancelled
:
1624 log('Darn SQLite LOCK!', sender
=self
, traceback
=True)
1626 # By the time we get here the update may have already been cancelled
1627 if not self
.feed_cache_update_cancelled
:
1628 callback_proc(channel
.title
, self
.updated_feeds
, total_channels
)
1630 self
.updated_feeds
+= 1
1631 self
.treeview_channel_set_color( channel
, 'default' )
1632 channel
.update_flag
= False
1635 if self
.updated_feeds
== total_channels
:
1638 def on_btnCancelFeedUpdate_clicked(self
, widget
):
1639 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
1640 self
.feed_cache_update_cancelled
= True
1642 def update_feed_cache(self
, channels
=None, force_update
=True,
1643 notify_no_new_episodes
=False, select_url_afterwards
=None):
1645 if self
.updating_feed_cache
:
1648 if not force_update
:
1649 self
.channels
= load_channels()
1650 self
.channel_list_changed
= True
1651 self
.updateComboBox()
1654 self
.updating_feed_cache
= True
1655 self
.itemUpdate
.set_sensitive(False)
1656 self
.itemUpdateChannel
.set_sensitive(False)
1659 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
1661 if channels
is None:
1662 channels
= self
.channels
1664 if len(channels
) == 1:
1665 text
= _('Updating %d feed.')
1667 text
= _('Updating %d feeds.')
1668 self
.pbFeedUpdate
.set_text( text
% len(channels
))
1669 self
.pbFeedUpdate
.set_fraction(0)
1671 # let's get down to business..
1672 callback_proc
= lambda title
, pos
, count
: util
.idle_add(
1673 self
.update_feed_cache_callback
, self
.pbFeedUpdate
, title
, pos
, count
)
1674 finish_proc
= lambda: util
.idle_add( self
.update_feed_cache_finish_callback
,
1675 channels
, notify_no_new_episodes
, select_url_afterwards
)
1677 self
.updated_feeds
= 0
1678 self
.feed_cache_update_cancelled
= False
1679 self
.btnUpdateFeeds
.hide_all()
1680 self
.hboxUpdateFeeds
.show_all()
1681 semaphore
= Semaphore(gl
.config
.max_simulaneous_feeds_updating
)
1683 for channel
in channels
:
1684 self
.treeview_channel_set_color( channel
, 'updating' )
1685 channel
.update_flag
= True
1686 args
= (channel
, len(channels
), semaphore
, callback_proc
, finish_proc
)
1687 thread
= Thread( target
= self
.update_feed_cache_proc
, args
= args
)
1690 def treeview_channel_set_color( self
, channel
, color
):
1691 if self
.treeChannels
.get_model():
1692 if color
in self
.channel_colors
:
1693 self
.treeChannels
.get_model().set(channel
.iter, 8, self
.channel_colors
[color
])
1695 self
.treeChannels
.get_model().set(channel
.iter, 8, color
)
1697 def on_gPodder_delete_event(self
, widget
, *args
):
1698 """Called when the GUI wants to close the window
1699 Displays a confirmation dialog (and closes/hides gPodder)
1702 downloading
= services
.download_status_manager
.has_items()
1704 # Only iconify if we are using the window's "X" button,
1705 # but not when we are using "Quit" in the menu or toolbar
1706 if not gl
.config
.on_quit_ask
and gl
.config
.on_quit_systray
and self
.tray_icon
and widget
.name
not in ('toolQuit', 'itemQuit'):
1707 self
.iconify_main_window()
1708 elif gl
.config
.on_quit_ask
or downloading
:
1709 if gpodder
.interface
== gpodder
.MAEMO
:
1710 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
1712 self
.close_gpodder()
1715 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
1716 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1717 dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
1719 title
= _('Quit gPodder')
1721 message
= _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1723 message
= _('Do you really want to quit gPodder now?')
1725 dialog
.set_title(title
)
1726 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
1728 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
1729 dialog
.vbox
.pack_start(cb_ask
)
1732 result
= dialog
.run()
1735 if result
== gtk
.RESPONSE_CLOSE
:
1736 if not downloading
and cb_ask
.get_active() == True:
1737 gl
.config
.on_quit_ask
= False
1738 self
.close_gpodder()
1740 self
.close_gpodder()
1744 def close_gpodder(self
):
1745 """ clean everything and exit properly
1748 if save_channels(self
.channels
):
1749 if gl
.config
.my_gpodder_autoupload
:
1750 log('Uploading to my.gpodder.org on close', sender
=self
)
1751 util
.idle_add(self
.on_upload_to_mygpo
, None)
1753 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1755 services
.download_status_manager
.cancel_all()
1757 while gtk
.events_pending():
1758 gtk
.main_iteration(False)
1762 self
.gtk_main_quit()
1765 def get_old_episodes(self
):
1767 for channel
in self
.channels
:
1768 for episode
in channel
.get_downloaded_episodes():
1769 if episode
.is_old() and not episode
.is_locked
and episode
.is_played
:
1770 episodes
.append(episode
)
1773 def for_each_selected_episode_url( self
, callback
):
1774 ( model
, paths
) = self
.treeAvailable
.get_selection().get_selected_rows()
1776 url
= model
.get_value( model
.get_iter( path
), 0)
1779 except Exception, e
:
1780 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url
, e
, sender
= self
)
1782 self
.update_selected_episode_list_icons()
1783 self
.updateComboBox(only_selected_channel
=True)
1785 def delete_episode_list( self
, episodes
, confirm
= True):
1786 if len(episodes
) == 0:
1789 if len(episodes
) == 1:
1790 message
= _('Do you really want to delete this episode?')
1792 message
= _('Do you really want to delete %d episodes?') % len(episodes
)
1794 if confirm
and self
.show_confirmation( message
, _('Delete episodes')) == False:
1797 episode_urls
= set()
1798 channel_urls
= set()
1799 for episode
in episodes
:
1800 log('Deleting episode: %s', episode
.title
, sender
= self
)
1801 episode
.delete_from_disk()
1802 episode_urls
.add(episode
.url
)
1803 channel_urls
.add(episode
.channel
.url
)
1805 self
.download_status_updated(episode_urls
, channel_urls
)
1807 def on_itemRemoveOldEpisodes_activate( self
, widget
):
1809 ('title_and_description', None, None, _('Episode')),
1810 ('channel_prop', None, None, _('Podcast')),
1811 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
1812 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
1813 ('played_prop', None, None, _('Status')),
1814 ('age_prop', None, None, _('Downloaded')),
1817 selection_buttons
= {
1818 _('Select played'): lambda episode
: episode
.is_played
,
1819 _('Select older than %d days') % gl
.config
.episode_old_age
: lambda episode
: episode
.is_old(),
1822 instructions
= _('Select the episodes you want to delete from your hard disk.')
1826 for channel
in self
.channels
:
1827 for episode
in channel
.get_downloaded_episodes():
1828 if not episode
.is_locked
:
1829 episodes
.append(episode
)
1830 selected
.append(episode
.is_played
)
1832 gPodderEpisodeSelector( title
= _('Remove old episodes'), instructions
= instructions
, \
1833 episodes
= episodes
, selected
= selected
, columns
= columns
, \
1834 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
1835 selection_buttons
= selection_buttons
)
1837 def mark_selected_episodes_new(self
):
1838 callback
= lambda url
: self
.active_channel
.find_episode(url
).mark_new()
1839 self
.for_each_selected_episode_url(callback
)
1841 def mark_selected_episodes_old(self
):
1842 callback
= lambda url
: self
.active_channel
.find_episode(url
).mark_old()
1843 self
.for_each_selected_episode_url(callback
)
1845 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
1847 callback
= lambda url
: db
.mark_episode(url
, is_played
=True, toggle
=True)
1849 callback
= lambda url
: db
.mark_episode(url
, is_played
=new_value
)
1851 self
.for_each_selected_episode_url(callback
)
1853 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
1855 callback
= lambda url
: db
.mark_episode(url
, is_locked
=True, toggle
=True)
1857 callback
= lambda url
: db
.mark_episode(url
, is_locked
=new_value
)
1859 self
.for_each_selected_episode_url(callback
)
1861 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
1862 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
1863 db
.update_channel_lock(self
.active_channel
)
1865 if self
.active_channel
.channel_is_locked
:
1866 self
.change_menu_item(self
.channel_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Allow deletion of all episodes'))
1868 self
.change_menu_item(self
.channel_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Prohibit deletion of all episodes'))
1870 for episode
in self
.active_channel
.get_all_episodes():
1871 db
.mark_episode(episode
.url
, is_locked
=self
.active_channel
.channel_is_locked
)
1873 self
.updateComboBox(only_selected_channel
=True)
1875 def on_item_email_subscriptions_activate(self
, widget
):
1876 if not self
.channels
:
1877 self
.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1878 elif not gl
.send_subscriptions():
1879 self
.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1881 def on_itemUpdateChannel_activate(self
, widget
=None):
1882 self
.update_feed_cache(channels
=[self
.active_channel
,])
1884 def on_itemUpdate_activate(self
, widget
, notify_no_new_episodes
=False):
1885 restore_from
= can_restore_from_opml()
1888 self
.update_feed_cache(notify_no_new_episodes
=notify_no_new_episodes
)
1889 elif restore_from
is not None:
1890 title
= _('Database upgrade required')
1891 message
= _('gPodder is now using a new (much faster) database backend and needs to convert your current data. This can take some time. Start the conversion now?')
1892 if self
.show_confirmation(message
, title
):
1893 add_callback
= lambda url
: self
.add_new_channel(url
, False, True)
1894 w
= gtk
.Dialog(_('Migrating to SQLite'), self
.gPodder
, 0, (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_ACCEPT
))
1895 w
.set_has_separator(False)
1896 w
.set_response_sensitive(gtk
.RESPONSE_ACCEPT
, False)
1897 w
.set_default_size(500, -1)
1898 pb
= gtk
.ProgressBar()
1901 l
.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1902 l
.set_alignment(0.0, 0.5)
1903 w
.vbox
.pack_start(l
)
1906 l
.set_alignment(0.0, 0.5)
1907 l
.set_text(_('Please wait while your settings are converted.'))
1908 w
.vbox
.pack_start(l
)
1909 w
.vbox
.pack_start(pb
)
1911 lb
.set_ellipsize(pango
.ELLIPSIZE_END
)
1912 lb
.set_alignment(0.0, 0.5)
1913 lb
.set_padding(6, 6)
1914 w
.vbox
.pack_start(lb
)
1916 def set_pb_status(pb
, lb
, fraction
, text
):
1917 pb
.set_fraction(float(fraction
)/100.0)
1918 pb
.set_text('%.0f %%' % fraction
)
1919 lb
.set_markup('<i>%s</i>' % saxutils
.escape(text
))
1920 while gtk
.events_pending():
1921 gtk
.main_iteration(False)
1922 status_callback
= lambda fraction
, text
: set_pb_status(pb
, lb
, fraction
, text
)
1923 get_localdb
= lambda channel
: LocalDBReader(channel
.url
).read(channel
.index_file
)
1925 start
= datetime
.datetime
.now()
1926 gl
.migrate_to_sqlite(add_callback
, status_callback
, load_channels
, get_localdb
)
1927 # Refresh the view with the updated episodes
1928 self
.updateComboBox()
1929 time_taken
= str(datetime
.datetime
.now()-start
)
1930 status_callback(100.0, _('Migration finished in %s') % time_taken
)
1931 w
.set_response_sensitive(gtk
.RESPONSE_ACCEPT
, True)
1935 gPodderWelcome(center_on_widget
=self
.gPodder
, show_example_podcasts_callback
=self
.on_itemImportChannels_activate
, setup_my_gpodder_callback
=self
.on_download_from_mygpo
)
1937 def download_episode_list(self
, episodes
):
1938 services
.download_status_manager
.start_batch_mode()
1939 for episode
in episodes
:
1940 log('Downloading episode: %s', episode
.title
, sender
= self
)
1941 filename
= episode
.local_filename()
1942 if not episode
.was_downloaded(and_exists
=True) and not services
.download_status_manager
.is_download_in_progress( episode
.url
):
1943 download
.DownloadThread(episode
.channel
, episode
, self
.notification
).start()
1944 services
.download_status_manager
.end_batch_mode()
1946 def new_episodes_show(self
, episodes
):
1948 ('title_and_description', None, None, _('Episode')),
1949 ('channel_prop', None, None, _('Podcast')),
1950 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
1951 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
1954 if len(episodes
) > 0:
1955 instructions
= _('Select the episodes you want to download now.')
1957 gPodderEpisodeSelector(title
=_('New episodes available'), instructions
=instructions
, \
1958 episodes
=episodes
, columns
=columns
, selected_default
=True, \
1959 stock_ok_button
= 'gpodder-download', \
1960 callback
=self
.download_episode_list
, \
1961 remove_callback
=lambda e
: e
.mark_old(), \
1962 remove_action
=_('Never download'), \
1963 remove_finished
=self
.episode_new_status_changed
)
1965 title
= _('No new episodes')
1966 message
= _('No new episodes to download.\nPlease check for new episodes later.')
1967 self
.show_message(message
, title
)
1969 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
1970 self
.download_all_new()
1972 def download_all_new(self
, channels
=None):
1973 if channels
is None:
1974 channels
= self
.channels
1976 for channel
in channels
:
1977 for episode
in channel
.get_new_episodes():
1978 episodes
.append(episode
)
1979 self
.new_episodes_show(episodes
)
1981 def get_all_episodes(self
, exclude_nonsignificant
=True ):
1982 """'exclude_nonsignificant' will exclude non-downloaded episodes
1983 and all episodes from channels that are set to skip when syncing"""
1985 for channel
in self
.channels
:
1986 if not channel
.sync_to_devices
and exclude_nonsignificant
:
1987 log('Skipping channel: %s', channel
.title
, sender
=self
)
1989 for episode
in channel
.get_all_episodes():
1990 if episode
.was_downloaded(and_exists
=True) or not exclude_nonsignificant
:
1991 episode_list
.append(episode
)
1994 def ipod_delete_played(self
, device
):
1995 all_episodes
= self
.get_all_episodes( exclude_nonsignificant
=False )
1996 episodes_on_device
= device
.get_all_tracks()
1997 for local_episode
in all_episodes
:
1998 device_episode
= device
.episode_on_device(local_episode
)
1999 if device_episode
and ( local_episode
.is_played
and not local_episode
.is_locked
2000 or local_episode
.state
== db
.STATE_DELETED
):
2001 log("mp3_player_delete_played: removing %s" % device_episode
.title
)
2002 device
.remove_track(device_episode
)
2004 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
2005 # make sure gpod is available before even trying to sync
2006 if gl
.config
.device_type
== 'ipod' and not sync
.gpod_available
:
2007 title
= _('Cannot Sync To iPod')
2008 message
= _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2009 self
.notification( message
, title
)
2011 elif gl
.config
.device_type
== 'mtp' and not sync
.pymtp_available
:
2012 title
= _('Cannot sync to MTP device')
2013 message
= _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2014 self
.notification( message
, title
)
2017 device
= sync
.open_device()
2018 device
.register( 'post-done', self
.sync_to_ipod_completed
)
2021 title
= _('No device configured')
2022 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2023 self
.notification(message
, title
)
2026 if not device
.open():
2027 title
= _('Cannot open device')
2028 message
= _('There has been an error opening your device.')
2029 self
.notification(message
, title
)
2032 if gl
.config
.ipod_purge_old_episodes
:
2035 sync_all_episodes
= not bool(episodes
)
2037 if episodes
is None:
2038 episodes
= self
.get_all_episodes()
2040 # make sure we have enough space on the device
2042 free_space
= device
.get_free_space()
2043 for episode
in episodes
:
2044 if not device
.episode_on_device(episode
) and not (sync_all_episodes
and gl
.config
.only_sync_not_played
and episode
.is_played
):
2045 total_size
+= util
.calculate_size(str(episode
.local_filename()))
2047 if total_size
> free_space
:
2048 # can be negative because of the 10 MiB for reserved for the iTunesDB
2049 free_space
= max( free_space
, 0 )
2050 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size
, free_space
)
2051 title
= _('Not enough space left on device.')
2052 message
= _('%s remaining on device.\nPlease free up %s and try again.' % (
2053 util
.format_filesize( free_space
), util
.format_filesize( total_size
- free_space
)))
2054 self
.notification(message
, title
)
2057 gPodderSync(device
=device
, gPodder
=self
)
2058 Thread(target
=self
.sync_to_ipod_thread
, args
=(widget
, device
, sync_all_episodes
, episodes
)).start()
2060 self
.tray_icon
.set_synchronisation_device(device
)
2062 def sync_to_ipod_completed(self
, device
, successful_sync
):
2063 device
.unregister( 'post-done', self
.sync_to_ipod_completed
)
2066 self
.tray_icon
.release_synchronisation_device()
2068 if not successful_sync
:
2069 title
= _('Error closing device')
2070 message
= _('There has been an error closing your device.')
2071 self
.notification(message
, title
)
2073 # update model for played state updates after sync
2074 util
.idle_add(self
.updateComboBox
)
2076 def sync_to_ipod_thread(self
, widget
, device
, sync_all_episodes
, episodes
=None):
2077 if sync_all_episodes
:
2078 device
.add_tracks(episodes
)
2079 # 'only_sync_not_played' must be used or else all the played
2080 # tracks will be copied then immediately deleted
2081 if gl
.config
.mp3_player_delete_played
and gl
.config
.only_sync_not_played
:
2082 self
.ipod_delete_played(device
)
2084 device
.add_tracks(episodes
, force_played
=True)
2087 def ipod_cleanup_callback(self
, device
, tracks
):
2088 title
= _('Delete podcasts from device?')
2089 message
= _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
2090 if len(tracks
) > 0 and self
.show_confirmation(message
, title
):
2091 device
.remove_tracks(tracks
)
2093 if not device
.close():
2094 title
= _('Error closing device')
2095 message
= _('There has been an error closing your device.')
2096 self
.show_message(message
, title
)
2099 def on_cleanup_ipod_activate(self
, widget
, *args
):
2101 ('title', None, None, _('Episode')),
2102 ('podcast', None, None, _('Podcast')),
2103 ('filesize', None, None, _('Size')),
2104 ('modified', None, None, _('Copied')),
2105 ('playcount', None, None, _('Play count')),
2106 ('released', None, None, _('Released')),
2109 device
= sync
.open_device()
2112 title
= _('No device configured')
2113 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2114 self
.show_message(message
, title
)
2117 if not device
.open():
2118 title
= _('Cannot open device')
2119 message
= _('There has been an error opening your device.')
2120 self
.show_message(message
, title
)
2123 gPodderSync(device
=device
, gPodder
=self
)
2125 tracks
= device
.get_all_tracks()
2127 remove_tracks_callback
= lambda tracks
: self
.ipod_cleanup_callback(device
, tracks
)
2129 for key
, sort_name
, sort_type
, caption
in columns
:
2130 want_this_column
= False
2131 for track
in tracks
:
2132 if getattr(track
, key
) is not None:
2133 want_this_column
= True
2136 if want_this_column
:
2137 wanted_columns
.append((key
, sort_name
, sort_type
, caption
))
2138 title
= _('Remove podcasts from device')
2139 instructions
= _('Select the podcast episodes you want to remove from your device.')
2140 gPodderEpisodeSelector(title
=title
, instructions
=instructions
, episodes
=tracks
, columns
=wanted_columns
, \
2141 stock_ok_button
=gtk
.STOCK_DELETE
, callback
=remove_tracks_callback
, tooltip_attribute
=None)
2143 title
= _('No files on device')
2144 message
= _('The devices contains no files to be removed.')
2145 self
.show_message(message
, title
)
2148 def on_manage_device_playlist(self
, widget
):
2149 # make sure gpod is available before even trying to sync
2150 if gl
.config
.device_type
== 'ipod' and not sync
.gpod_available
:
2151 title
= _('Cannot manage iPod playlist')
2152 message
= _('This feature is not available for iPods.')
2153 self
.notification( message
, title
)
2155 elif gl
.config
.device_type
== 'mtp' and not sync
.pymtp_available
:
2156 title
= _('Cannot manage MTP device playlist')
2157 message
= _('This feature is not available for MTP devices.')
2158 self
.notification( message
, title
)
2161 device
= sync
.open_device()
2164 title
= _('No device configured')
2165 message
= _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2166 self
.notification(message
, title
)
2169 if not device
.open():
2170 title
= _('Cannot open device')
2171 message
= _('There has been an error opening your device.')
2172 self
.notification(message
, title
)
2175 gPodderPlaylist(device
=device
, gPodder
=self
)
2178 def show_hide_tray_icon(self
):
2179 if gl
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
2180 self
.tray_icon
= trayicon
.GPodderStatusIcon(self
, scalable_dir
)
2181 elif not gl
.config
.display_tray_icon
and self
.tray_icon
is not None:
2182 self
.tray_icon
.set_visible(False)
2184 self
.tray_icon
= None
2186 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
2187 self
.tray_icon
.set_visible(self
.minimized
)
2188 elif self
.tray_icon
:
2189 self
.tray_icon
.set_visible(True)
2191 def on_itemShowToolbar_activate(self
, widget
):
2192 gl
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
2194 def on_itemShowDescription_activate(self
, widget
):
2195 gl
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
2197 def update_item_device( self
):
2198 if gl
.config
.device_type
!= 'none':
2199 self
.itemDevice
.show_all()
2200 (label
,) = self
.itemDevice
.get_children()
2201 label
.set_text(gl
.get_device_name())
2203 self
.itemDevice
.hide_all()
2205 def properties_closed( self
):
2206 self
.show_hide_tray_icon()
2207 self
.update_item_device()
2208 self
.updateComboBox()
2210 def on_itemPreferences_activate(self
, widget
, *args
):
2211 if gpodder
.interface
== gpodder
.GUI
:
2212 gPodderProperties(callback_finished
=self
.properties_closed
, user_apps_reader
=self
.user_apps_reader
)
2214 gPodderMaemoPreferences()
2216 def on_itemDependencies_activate(self
, widget
):
2217 gPodderDependencyManager()
2219 def on_add_new_google_search(self
, widget
, *args
):
2220 def add_google_video_search(query
):
2221 self
.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib
.quote(query
)+'&so=1&num=250&output=rss')
2223 gPodderAddPodcastDialog(url_callback
=add_google_video_search
, custom_title
=_('Add Google Video search'), custom_label
=_('Search for:'))
2225 def on_upgrade_from_videocenter(self
, widget
):
2226 from gpodder
import nokiavideocenter
2227 vc
= nokiavideocenter
.UpgradeFromVideocenter()
2229 gPodderOpmlLister(custom_title
=_('Import podcasts from Video Center'), hide_url_entry
=True).get_channels_from_url(vc
.opmlfile
, lambda url
: self
.add_new_channel(url
,False,block
=True), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
2231 self
.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2233 def require_my_gpodder_authentication(self
):
2234 if not gl
.config
.my_gpodder_username
or not gl
.config
.my_gpodder_password
:
2235 success
, authentication
= self
.UsernamePasswordDialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username
=gl
.config
.my_gpodder_username
, password
=gl
.config
.my_gpodder_password
, username_prompt
=_('E-Mail Address'), register_callback
=lambda: util
.open_website('http://my.gpodder.org/register'))
2236 if success
and authentication
[0] and authentication
[1]:
2237 gl
.config
.my_gpodder_username
, gl
.config
.my_gpodder_password
= authentication
2244 def my_gpodder_offer_autoupload(self
):
2245 if not gl
.config
.my_gpodder_autoupload
:
2246 if self
.show_confirmation(_('gPodder can automatically upload your subscription list to my.gpodder.org when you close it. Do you want to enable this feature?'), _('Upload subscriptions on quit')):
2247 gl
.config
.my_gpodder_autoupload
= True
2249 def on_download_from_mygpo(self
, widget
):
2250 if self
.require_my_gpodder_authentication():
2251 client
= my
.MygPodderClient(gl
.config
.my_gpodder_username
, gl
.config
.my_gpodder_password
)
2252 opml_data
= client
.download_subscriptions()
2253 if len(opml_data
) > 0:
2254 fp
= open(gl
.channel_opml_file
, 'w')
2257 (added
, skipped
) = (0, 0)
2258 i
= opml
.Importer(gl
.channel_opml_file
)
2259 for item
in i
.items
:
2261 if url
not in (c
.url
for c
in self
.channels
):
2262 self
.add_new_channel(url
, ask_download_new
=False, block
=True)
2265 log('Already added: %s', url
, sender
=self
)
2267 self
.updateComboBox()
2269 self
.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added
, skipped
), _('Result of subscription download'))
2270 elif widget
is not None:
2271 self
.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2272 self
.my_gpodder_offer_autoupload()
2274 gl
.config
.my_gpodder_password
= ''
2275 self
.on_download_from_mygpo(widget
)
2277 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2279 def on_upload_to_mygpo(self
, widget
):
2280 if self
.require_my_gpodder_authentication():
2281 client
= my
.MygPodderClient(gl
.config
.my_gpodder_username
, gl
.config
.my_gpodder_password
)
2282 save_channels(self
.channels
)
2283 success
, messages
= client
.upload_subscriptions(gl
.channel_opml_file
)
2284 if widget
is not None:
2285 self
.show_message('\n'.join(messages
), _('Results of upload'))
2287 gl
.config
.my_gpodder_password
= ''
2288 self
.on_upload_to_mygpo(widget
)
2290 self
.my_gpodder_offer_autoupload()
2292 log('Upload to my.gpodder.org failed, but widget is None!', sender
=self
)
2293 elif widget
is not None:
2294 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2296 def on_itemAddChannel_activate(self
, widget
, *args
):
2297 gPodderAddPodcastDialog(url_callback
=self
.add_new_channel
)
2299 def on_itemEditChannel_activate(self
, widget
, *args
):
2300 if self
.active_channel
is None:
2301 title
= _('No podcast selected')
2302 message
= _('Please select a podcast in the podcasts list to edit.')
2303 self
.show_message( message
, title
)
2306 gPodderChannel(channel
=self
.active_channel
, callback_closed
=lambda: self
.updateComboBox(only_selected_channel
=True), callback_change_url
=self
.change_channel_url
)
2308 def change_channel_url(self
, old_url
, new_url
):
2311 channel
= podcastChannel
.load(url
=new_url
, create
=True)
2316 self
.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2319 for channel
in self
.channels
:
2320 if channel
.url
== old_url
:
2321 log('=> change channel url from %s to %s', old_url
, new_url
)
2322 old_save_dir
= channel
.save_dir
2323 channel
.url
= new_url
2324 new_save_dir
= channel
.save_dir
2325 log('old save dir=%s', old_save_dir
, sender
=self
)
2326 log('new save dir=%s', new_save_dir
, sender
=self
)
2327 files
= glob
.glob(os
.path
.join(old_save_dir
, '*'))
2328 log('moving %d files to %s', len(files
), new_save_dir
, sender
=self
)
2330 log('moving %s', file, sender
=self
)
2331 shutil
.move(file, new_save_dir
)
2333 os
.rmdir(old_save_dir
)
2335 log('Warning: cannot delete %s', old_save_dir
, sender
=self
)
2337 save_channels(self
.channels
)
2338 # update feed cache and select the podcast with the new URL afterwards
2339 self
.update_feed_cache(force_update
=False, select_url_afterwards
=new_url
)
2341 def on_itemRemoveChannel_activate(self
, widget
, *args
):
2343 if gpodder
.interface
== gpodder
.GUI
:
2344 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2345 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
2346 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
2348 title
= _('Remove podcast and episodes?')
2349 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
2351 dialog
.set_title(title
)
2352 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2354 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
2355 dialog
.vbox
.pack_start(cb_ask
)
2357 affirmative
= gtk
.RESPONSE_YES
2358 elif gpodder
.interface
== gpodder
.MAEMO
:
2359 cb_ask
= gtk
.CheckButton('') # dummy check button
2360 dialog
= hildon
.Note('confirmation', (self
.gPodder
, _('Do you really want to remove this podcast and all downloaded episodes?')))
2361 affirmative
= gtk
.RESPONSE_OK
2363 result
= dialog
.run()
2366 if result
== affirmative
:
2367 # delete downloaded episodes only if checkbox is unchecked
2368 if cb_ask
.get_active() == False:
2369 self
.active_channel
.remove_downloaded()
2371 log('Not removing downloaded episodes', sender
=self
)
2373 # only delete partial files if we do not have any downloads in progress
2374 delete_partial
= not services
.download_status_manager
.has_items()
2375 gl
.clean_up_downloads(delete_partial
)
2377 # cancel any active downloads from this channel
2378 if not delete_partial
:
2379 for episode
in self
.active_channel
.get_all_episodes():
2380 services
.download_status_manager
.cancel_by_url(episode
.url
)
2382 # get the URL of the podcast we want to select next
2383 position
= self
.channels
.index(self
.active_channel
)
2384 if position
== len(self
.channels
)-1:
2385 # this is the last podcast, so select the URL
2386 # of the item before this one (i.e. the "new last")
2387 select_url
= self
.channels
[position
-1].url
2389 # there is a podcast after the deleted one, so
2390 # we simply select the one that comes after it
2391 select_url
= self
.channels
[position
+1].url
2393 # Remove the channel
2394 self
.active_channel
.delete()
2395 self
.channels
.remove(self
.active_channel
)
2396 self
.channel_list_changed
= True
2397 save_channels(self
.channels
)
2399 # Re-load the channels and select the desired new channel
2400 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
2402 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
2403 self
.update_podcasts_tab()
2405 def get_opml_filter(self
):
2406 filter = gtk
.FileFilter()
2407 filter.add_pattern('*.opml')
2408 filter.add_pattern('*.xml')
2409 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2412 def on_item_import_from_file_activate(self
, widget
, filename
=None):
2413 if filename
is None:
2414 if gpodder
.interface
== gpodder
.GUI
:
2415 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2416 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2417 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2418 elif gpodder
.interface
== gpodder
.MAEMO
:
2419 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2420 dlg
.set_filter(self
.get_opml_filter())
2421 response
= dlg
.run()
2423 if response
== gtk
.RESPONSE_OK
:
2424 filename
= dlg
.get_filename()
2427 if filename
is not None:
2428 gPodderOpmlLister(custom_title
=_('Import podcasts from OPML file'), hide_url_entry
=True).get_channels_from_url(filename
, lambda url
: self
.add_new_channel(url
,False,block
=True), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
2430 def on_itemExportChannels_activate(self
, widget
, *args
):
2431 if not self
.channels
:
2432 title
= _('Nothing to export')
2433 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2434 self
.show_message( message
, title
)
2437 if gpodder
.interface
== gpodder
.GUI
:
2438 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
2439 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2440 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
2441 elif gpodder
.interface
== gpodder
.MAEMO
:
2442 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
2443 dlg
.set_filter(self
.get_opml_filter())
2444 response
= dlg
.run()
2445 if response
== gtk
.RESPONSE_OK
:
2446 filename
= dlg
.get_filename()
2448 exporter
= opml
.Exporter( filename
)
2449 if exporter
.write(self
.channels
):
2450 if len(self
.channels
) == 1:
2451 title
= _('One subscription exported')
2453 title
= _('%d subscriptions exported') % len(self
.channels
)
2454 self
.show_message(_('Your podcast list has been successfully exported.'), title
)
2456 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2460 def on_itemImportChannels_activate(self
, widget
, *args
):
2461 gPodderOpmlLister().get_channels_from_url(gl
.config
.opml_url
, lambda url
: self
.add_new_channel(url
,False,block
=True), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
2463 def on_homepage_activate(self
, widget
, *args
):
2464 util
.open_website(app_website
)
2466 def on_wiki_activate(self
, widget
, *args
):
2467 util
.open_website('http://wiki.gpodder.org/')
2469 def on_bug_tracker_activate(self
, widget
, *args
):
2470 util
.open_website('http://bugs.gpodder.org/')
2472 def on_itemAbout_activate(self
, widget
, *args
):
2473 dlg
= gtk
.AboutDialog()
2474 dlg
.set_name(app_name
.replace('p', 'P')) # gpodder->gPodder
2475 dlg
.set_version( app_version
)
2476 dlg
.set_copyright( app_copyright
)
2477 dlg
.set_website( app_website
)
2478 dlg
.set_translator_credits( _('translator-credits'))
2479 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
2481 if gpodder
.interface
== gpodder
.GUI
:
2482 # For the "GUI" version, we add some more
2483 # items to the about dialog (credits and logo)
2484 dlg
.set_authors(app_authors
)
2486 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(scalable_dir
))
2492 def on_wNotebook_switch_page(self
, widget
, *args
):
2494 if gpodder
.interface
== gpodder
.MAEMO
:
2495 page
= self
.wNotebook
.get_nth_page(page_num
)
2496 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2497 if page_num
== 0 and self
.active_channel
is not None:
2498 self
.set_title(self
.active_channel
.title
)
2500 self
.set_title(tab_label
)
2502 self
.play_or_download()
2504 self
.toolDownload
.set_sensitive( False)
2505 self
.toolPlay
.set_sensitive( False)
2506 self
.toolTransfer
.set_sensitive( False)
2507 self
.toolCancel
.set_sensitive( services
.download_status_manager
.has_items())
2509 def on_treeChannels_row_activated(self
, widget
, *args
):
2510 # double-click action of the podcast list
2513 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2514 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2516 if model
is not None and iter is not None:
2517 old_active_channel
= self
.active_channel
2518 (id,) = model
.get_path(iter)
2519 self
.active_channel
= self
.channels
[id]
2521 if self
.active_channel
== old_active_channel
:
2524 if gpodder
.interface
== gpodder
.MAEMO
:
2525 self
.set_title(self
.active_channel
.title
)
2526 self
.itemEditChannel
.show_all()
2527 self
.itemRemoveChannel
.show_all()
2528 self
.channel_toggle_lock
.show_all()
2529 if self
.active_channel
.channel_is_locked
:
2530 self
.change_menu_item(self
.channel_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Allow deletion of all episodes'))
2532 self
.change_menu_item(self
.channel_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Prohibit deletion of all episodes'))
2535 self
.active_channel
= None
2536 self
.itemEditChannel
.hide_all()
2537 self
.itemRemoveChannel
.hide_all()
2538 self
.channel_toggle_lock
.hide_all()
2540 self
.updateTreeView()
2542 def on_entryAddChannel_changed(self
, widget
, *args
):
2543 active
= self
.entryAddChannel
.get_text() not in ('', self
.ENTER_URL_TEXT
)
2544 self
.btnAddChannel
.set_sensitive( active
)
2546 def on_btnAddChannel_clicked(self
, widget
, *args
):
2547 url
= self
.entryAddChannel
.get_text()
2548 self
.entryAddChannel
.set_text('')
2549 self
.add_new_channel( url
)
2551 def on_btnEditChannel_clicked(self
, widget
, *args
):
2552 self
.on_itemEditChannel_activate( widget
, args
)
2554 def on_treeAvailable_row_activated(self
, widget
, path
=None, view_column
=None):
2556 What this function does depends on from which widget it is called.
2557 It gets the selected episodes of the current podcast and runs one
2558 of the following actions on them:
2560 * Transfer (to MP3 player, iPod, etc..)
2561 * Playback/open files
2562 * Show the episode info dialog
2566 selection
= self
.treeAvailable
.get_selection()
2567 (model
, paths
) = selection
.get_selected_rows()
2570 log('Nothing selected', sender
=self
)
2573 wname
= widget
.get_name()
2574 do_transfer
= (wname
in ('itemTransferSelected', 'toolTransfer'))
2575 do_playback
= (wname
in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2576 do_epdialog
= (wname
in ('treeAvailable', 'item_episode_details'))
2580 it
= model
.get_iter(path
)
2581 url
= model
.get_value(it
, 0)
2582 episode
= self
.active_channel
.find_episode(url
)
2583 episodes
.append(episode
)
2585 if len(episodes
) == 0:
2586 log('No episodes selected', sender
=self
)
2589 self
.on_sync_to_ipod_activate(widget
, episodes
)
2591 for episode
in episodes
:
2592 # Make sure to mark the episode as downloaded
2593 if os
.path
.exists(episode
.local_filename()):
2594 episode
.channel
.addDownloadedItem(episode
)
2595 self
.playback_episode(episode
)
2596 elif gl
.config
.enable_streaming
:
2597 self
.playback_episode(episode
, stream
=True)
2599 play_callback
= lambda: self
.playback_episode(episode
)
2600 def download_callback():
2601 self
.download_episode_list([episode
])
2602 self
.play_or_download()
2603 if self
.gpodder_episode_window
is None:
2604 log('First-time use of episode window --- creating', sender
=self
)
2605 self
.gpodder_episode_window
= gPodderEpisode()
2606 self
.gpodder_episode_window
.show(episode
=episode
, download_callback
=download_callback
, play_callback
=play_callback
)
2608 self
.download_episode_list(episodes
)
2609 self
.play_or_download()
2611 log('Error in on_treeAvailable_row_activated', traceback
=True, sender
=self
)
2613 def on_treeAvailable_button_release_event(self
, widget
, *args
):
2614 self
.play_or_download()
2616 def auto_update_procedure(self
, first_run
=False):
2617 log('auto_update_procedure() got called', sender
=self
)
2618 if not first_run
and gl
.config
.auto_update_feeds
and self
.minimized
:
2619 self
.update_feed_cache(force_update
=True)
2621 next_update
= 60*1000*gl
.config
.auto_update_frequency
2622 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2624 def on_treeDownloads_row_activated(self
, widget
, *args
):
2627 if self
.wNotebook
.get_current_page() > 0:
2628 # Use the download list treeview + model
2629 ( tree
, column
) = ( self
.treeDownloads
, 3 )
2631 # Use the available podcasts treeview + model
2632 ( tree
, column
) = ( self
.treeAvailable
, 0 )
2634 selection
= tree
.get_selection()
2635 (model
, paths
) = selection
.get_selected_rows()
2637 url
= model
.get_value( model
.get_iter( path
), column
)
2638 cancel_urls
.append( url
)
2640 if len( cancel_urls
) == 0:
2641 log('Nothing selected.', sender
= self
)
2644 if len( cancel_urls
) == 1:
2645 title
= _('Cancel download?')
2646 message
= _("Cancelling this download will remove the partially downloaded file and stop the download.")
2648 title
= _('Cancel downloads?')
2649 message
= _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection
.count_selected_rows()
2651 if self
.show_confirmation( message
, title
):
2652 services
.download_status_manager
.start_batch_mode()
2653 for url
in cancel_urls
:
2654 services
.download_status_manager
.cancel_by_url( url
)
2655 services
.download_status_manager
.end_batch_mode()
2656 self
.play_or_download()
2658 def on_btnCancelDownloadStatus_clicked(self
, widget
, *args
):
2659 self
.on_treeDownloads_row_activated( widget
, None)
2661 def on_btnCancelAll_clicked(self
, widget
, *args
):
2662 self
.treeDownloads
.get_selection().select_all()
2663 self
.on_treeDownloads_row_activated( self
.toolCancel
, None)
2664 self
.treeDownloads
.get_selection().unselect_all()
2666 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2667 if self
.active_channel
is None:
2670 channel_url
= self
.active_channel
.url
2671 selection
= self
.treeAvailable
.get_selection()
2672 ( model
, paths
) = selection
.get_selected_rows()
2674 if selection
.count_selected_rows() == 0:
2675 log( 'Nothing selected - will not remove any downloaded episode.')
2678 if selection
.count_selected_rows() == 1:
2679 episode_title
= saxutils
.escape(model
.get_value(model
.get_iter(paths
[0]), 1))
2681 episode
= db
.load_episode(model
.get_value(model
.get_iter(paths
[0]), 0))
2682 if episode
['is_locked']:
2683 title
= _('%s is locked') % episode_title
2684 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2685 self
.notification(message
, title
)
2688 title
= _('Remove %s?') % episode_title
2689 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.")
2691 title
= _('Remove %d episodes?') % selection
.count_selected_rows()
2692 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.')
2696 episode
= db
.load_episode(model
.get_value(model
.get_iter(path
), 0))
2697 if episode
['is_locked']:
2700 if selection
.count_selected_rows() == locked_count
:
2701 title
= _('Episodes are locked')
2702 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2703 self
.notification(message
, title
)
2705 elif locked_count
> 0:
2706 title
= _('Remove %d out of %d episodes?') % (selection
.count_selected_rows() - locked_count
, selection
.count_selected_rows())
2707 message
= _('The selection contains locked episodes that will not be deleted. If you want to listen to the deleted episodes, you will have to re-download them.')
2709 # if user confirms deletion, let's remove some stuff ;)
2710 if self
.show_confirmation( message
, title
):
2712 # iterate over the selection, see also on_treeDownloads_row_activated
2714 url
= model
.get_value( model
.get_iter( path
), 0)
2715 self
.active_channel
.delete_episode_by_url( url
)
2717 # now, clear local db cache so we can re-read it
2718 self
.updateComboBox()
2720 log( 'Error while deleting (some) downloads.')
2722 # only delete partial files if we do not have any downloads in progress
2723 delete_partial
= not services
.download_status_manager
.has_items()
2724 gl
.clean_up_downloads(delete_partial
)
2725 self
.update_selected_episode_list_icons()
2726 self
.play_or_download()
2728 def on_key_press(self
, widget
, event
):
2729 # Allow tab switching with Ctrl + PgUp/PgDown
2730 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
2731 if event
.keyval
== gtk
.keysyms
.Page_Up
:
2732 self
.wNotebook
.prev_page()
2734 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
2735 self
.wNotebook
.next_page()
2738 # After this code we only handle Maemo hardware keys,
2739 # so if we are not a Maemo app, we don't do anything
2740 if gpodder
.interface
!= gpodder
.MAEMO
:
2743 if event
.keyval
== gtk
.keysyms
.F6
:
2745 self
.window
.unfullscreen()
2747 self
.window
.fullscreen()
2748 if event
.keyval
== gtk
.keysyms
.Escape
:
2749 new_visibility
= not self
.vboxChannelNavigator
.get_property('visible')
2750 self
.vboxChannelNavigator
.set_property('visible', new_visibility
)
2751 self
.column_size
.set_visible(not new_visibility
)
2752 self
.column_released
.set_visible(not new_visibility
)
2755 if event
.keyval
== gtk
.keysyms
.F7
: #plus
2757 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
2761 selection
= self
.treeChannels
.get_selection()
2762 (model
, iter) = selection
.get_selected()
2763 selection
.select_path(((model
.get_path(iter)[0]+diff
)%len(model
),))
2764 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2766 def window_state_event(self
, widget
, event
):
2767 if event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_FULLSCREEN
:
2768 self
.fullscreen
= True
2770 self
.fullscreen
= False
2772 old_minimized
= self
.minimized
2774 self
.minimized
= bool(event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_ICONIFIED
)
2775 if gpodder
.interface
== gpodder
.MAEMO
:
2776 self
.minimized
= bool(event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_WITHDRAWN
)
2778 if old_minimized
!= self
.minimized
and self
.tray_icon
:
2779 self
.gPodder
.set_skip_taskbar_hint(self
.minimized
)
2780 elif not self
.tray_icon
:
2781 self
.gPodder
.set_skip_taskbar_hint(False)
2783 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
2784 self
.tray_icon
.set_visible(self
.minimized
)
2786 def uniconify_main_window(self
):
2788 self
.gPodder
.present()
2790 def iconify_main_window(self
):
2791 if not self
.minimized
:
2792 self
.gPodder
.iconify()
2794 def update_podcasts_tab(self
):
2795 if len(self
.channels
):
2796 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
2798 self
.label2
.set_text(_('Podcasts'))
2800 class gPodderChannel(GladeWidget
):
2801 finger_friendly_widgets
= ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
2804 global WEB_BROWSER_ICON
2805 self
.changed
= False
2806 self
.image3167
.set_property('icon-name', WEB_BROWSER_ICON
)
2807 self
.gPodderChannel
.set_title( self
.channel
.title
)
2808 self
.entryTitle
.set_text( self
.channel
.title
)
2809 self
.entryURL
.set_text( self
.channel
.url
)
2811 self
.LabelDownloadTo
.set_text( self
.channel
.save_dir
)
2812 self
.LabelWebsite
.set_text( self
.channel
.link
)
2814 self
.cbNoSync
.set_active( not self
.channel
.sync_to_devices
)
2815 self
.musicPlaylist
.set_text(self
.channel
.device_playlist_name
)
2816 if self
.channel
.username
:
2817 self
.FeedUsername
.set_text( self
.channel
.username
)
2818 if self
.channel
.password
:
2819 self
.FeedPassword
.set_text( self
.channel
.password
)
2821 services
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
2822 services
.cover_downloader
.request_cover(self
.channel
)
2824 # Hide the website button if we don't have a valid URL
2825 if not self
.channel
.link
:
2826 self
.btn_website
.hide_all()
2828 b
= gtk
.TextBuffer()
2829 b
.set_text( self
.channel
.description
)
2830 self
.channel_description
.set_buffer( b
)
2832 #Add Drag and Drop Support
2833 flags
= gtk
.DEST_DEFAULT_ALL
2834 targets
= [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2835 actions
= gtk
.gdk
.ACTION_DEFAULT | gtk
.gdk
.ACTION_COPY
2836 self
.vboxCoverEditor
.drag_dest_set( flags
, targets
, actions
)
2837 self
.vboxCoverEditor
.connect( 'drag_data_received', self
.drag_data_received
)
2839 def on_btn_website_clicked(self
, widget
):
2840 util
.open_website(self
.channel
.link
)
2842 def on_btnDownloadCover_clicked(self
, widget
):
2843 if gpodder
.interface
== gpodder
.GUI
:
2844 dlg
= gtk
.FileChooserDialog(title
=_('Select new podcast cover artwork'), parent
=self
.gPodderChannel
, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2845 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2846 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2847 elif gpodder
.interface
== gpodder
.MAEMO
:
2848 dlg
= hildon
.FileChooserDialog(self
.gPodderChannel
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2850 if dlg
.run() == gtk
.RESPONSE_OK
:
2852 services
.cover_downloader
.replace_cover(self
.channel
, url
)
2856 def on_btnClearCover_clicked(self
, widget
):
2857 services
.cover_downloader
.replace_cover(self
.channel
)
2859 def cover_download_finished(self
, channel_url
, pixbuf
):
2860 if pixbuf
is not None:
2861 self
.imgCover
.set_from_pixbuf(pixbuf
)
2862 self
.gPodderChannel
.show()
2864 def drag_data_received( self
, widget
, content
, x
, y
, sel
, ttype
, time
):
2865 files
= sel
.data
.strip().split('\n')
2867 self
.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2872 if file.startswith('file://') or file.startswith('http://'):
2873 services
.cover_downloader
.replace_cover(self
.channel
, file)
2876 self
.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2878 def on_gPodderChannel_destroy(self
, widget
, *args
):
2879 services
.cover_downloader
.unregister('cover-available', self
.cover_download_finished
)
2881 def on_btnOK_clicked(self
, widget
, *args
):
2882 entered_url
= self
.entryURL
.get_text()
2883 channel_url
= self
.channel
.url
2885 if entered_url
!= channel_url
:
2886 if self
.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils
.escape(entered_url
),), _('Really change URL?')):
2887 if hasattr(self
, 'callback_change_url'):
2888 self
.gPodderChannel
.hide_all()
2889 self
.callback_change_url(channel_url
, entered_url
)
2891 self
.channel
.sync_to_devices
= not self
.cbNoSync
.get_active()
2892 self
.channel
.device_playlist_name
= self
.musicPlaylist
.get_text()
2893 self
.channel
.set_custom_title( self
.entryTitle
.get_text())
2894 self
.channel
.username
= self
.FeedUsername
.get_text().strip()
2895 self
.channel
.password
= self
.FeedPassword
.get_text()
2898 self
.gPodderChannel
.destroy()
2899 self
.callback_closed()
2901 class gPodderAddPodcastDialog(GladeWidget
):
2902 finger_friendly_widgets
= ['btn_close', 'btn_add']
2905 if not hasattr(self
, 'url_callback'):
2906 log('No url callback set', sender
=self
)
2907 self
.url_callback
= None
2908 if hasattr(self
, 'custom_label'):
2909 self
.label_add
.set_text(self
.custom_label
)
2910 if hasattr(self
, 'custom_title'):
2911 self
.gPodderAddPodcastDialog
.set_title(self
.custom_title
)
2912 if gpodder
.interface
== gpodder
.MAEMO
:
2913 self
.entry_url
.set_text('http://')
2915 def on_btn_close_clicked(self
, widget
):
2916 self
.gPodderAddPodcastDialog
.destroy()
2918 def on_entry_url_changed(self
, widget
):
2919 self
.btn_add
.set_sensitive(self
.entry_url
.get_text().strip() != '')
2921 def on_btn_add_clicked(self
, widget
):
2922 url
= self
.entry_url
.get_text()
2923 self
.on_btn_close_clicked(widget
)
2924 if self
.url_callback
is not None:
2925 self
.url_callback(url
)
2928 class gPodderMaemoPreferences(GladeWidget
):
2929 finger_friendly_widgets
= ['btn_close', 'label128', 'label129', 'btn_advanced']
2932 gl
.config
.connect_gtk_togglebutton('update_on_startup', self
.update_on_startup
)
2933 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.show_tray_icon
)
2934 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.show_notifications
)
2935 gl
.config
.connect_gtk_togglebutton('on_quit_ask', self
.on_quit_ask
)
2937 self
.restart_required
= False
2938 self
.show_tray_icon
.connect('clicked', self
.on_restart_required
)
2939 self
.show_notifications
.connect('clicked', self
.on_restart_required
)
2941 def on_restart_required(self
, widget
):
2942 self
.restart_required
= True
2944 def on_btn_advanced_clicked(self
, widget
):
2945 self
.gPodderMaemoPreferences
.destroy()
2946 gPodderConfigEditor()
2948 def on_btn_close_clicked(self
, widget
):
2949 self
.gPodderMaemoPreferences
.destroy()
2950 if self
.restart_required
:
2951 self
.show_message(_('Please restart gPodder for the changes to take effect.'))
2954 class gPodderProperties(GladeWidget
):
2956 if not hasattr( self
, 'callback_finished'):
2957 self
.callback_finished
= None
2959 if gpodder
.interface
== gpodder
.MAEMO
:
2960 self
.table5
.hide_all() # player
2961 self
.gPodderProperties
.fullscreen()
2963 gl
.config
.connect_gtk_editable( 'http_proxy', self
.httpProxy
)
2964 gl
.config
.connect_gtk_editable( 'ftp_proxy', self
.ftpProxy
)
2965 gl
.config
.connect_gtk_editable( 'player', self
.openApp
)
2966 gl
.config
.connect_gtk_editable('videoplayer', self
.openVideoApp
)
2967 gl
.config
.connect_gtk_editable( 'custom_sync_name', self
.entryCustomSyncName
)
2968 gl
.config
.connect_gtk_togglebutton( 'custom_sync_name_enabled', self
.cbCustomSyncName
)
2969 gl
.config
.connect_gtk_togglebutton( 'auto_download_when_minimized', self
.downloadnew
)
2970 gl
.config
.connect_gtk_togglebutton( 'update_on_startup', self
.updateonstartup
)
2971 gl
.config
.connect_gtk_togglebutton( 'only_sync_not_played', self
.only_sync_not_played
)
2972 gl
.config
.connect_gtk_togglebutton( 'fssync_channel_subfolders', self
.cbChannelSubfolder
)
2973 gl
.config
.connect_gtk_togglebutton( 'on_sync_mark_played', self
.on_sync_mark_played
)
2974 gl
.config
.connect_gtk_togglebutton( 'on_sync_delete', self
.on_sync_delete
)
2975 gl
.config
.connect_gtk_togglebutton( 'proxy_use_environment', self
.cbEnvironmentVariables
)
2976 gl
.config
.connect_gtk_spinbutton('episode_old_age', self
.episode_old_age
)
2977 gl
.config
.connect_gtk_togglebutton('auto_remove_old_episodes', self
.auto_remove_old_episodes
)
2978 gl
.config
.connect_gtk_togglebutton('auto_update_feeds', self
.auto_update_feeds
)
2979 gl
.config
.connect_gtk_spinbutton('auto_update_frequency', self
.auto_update_frequency
)
2980 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.display_tray_icon
)
2981 gl
.config
.connect_gtk_togglebutton('minimize_to_tray', self
.minimize_to_tray
)
2982 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.enable_notifications
)
2983 gl
.config
.connect_gtk_togglebutton('start_iconified', self
.start_iconified
)
2984 gl
.config
.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self
.ipod_write_gtkpod_extended
)
2985 gl
.config
.connect_gtk_togglebutton('mp3_player_delete_played', self
.delete_episodes_marked_played
)
2987 self
.enable_notifications
.set_sensitive(self
.display_tray_icon
.get_active())
2988 self
.minimize_to_tray
.set_sensitive(self
.display_tray_icon
.get_active())
2990 self
.entryCustomSyncName
.set_sensitive( self
.cbCustomSyncName
.get_active())
2992 self
.iPodMountpoint
.set_label( gl
.config
.ipod_mount
)
2993 self
.filesystemMountpoint
.set_label( gl
.config
.mp3_player_folder
)
2994 self
.chooserDownloadTo
.set_current_folder(gl
.downloaddir
)
2996 self
.on_sync_delete
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2997 self
.on_sync_mark_played
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2999 if tagging_supported():
3000 gl
.config
.connect_gtk_togglebutton( 'update_tags', self
.updatetags
)
3002 self
.updatetags
.set_sensitive( False)
3003 new_label
= '%s (%s)' % ( self
.updatetags
.get_label(), _('needs python-eyed3') )
3004 self
.updatetags
.set_label( new_label
)
3007 self
.comboboxDeviceType
.set_active( 0)
3008 if gl
.config
.device_type
== 'ipod':
3009 self
.comboboxDeviceType
.set_active( 1)
3010 elif gl
.config
.device_type
== 'filesystem':
3011 self
.comboboxDeviceType
.set_active( 2)
3012 elif gl
.config
.device_type
== 'mtp':
3013 self
.comboboxDeviceType
.set_active( 3)
3015 # setup cell renderers
3016 cellrenderer
= gtk
.CellRendererPixbuf()
3017 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, False)
3018 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
3019 cellrenderer
= gtk
.CellRendererText()
3020 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, True)
3021 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
3023 cellrenderer
= gtk
.CellRendererPixbuf()
3024 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, False)
3025 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
3026 cellrenderer
= gtk
.CellRendererText()
3027 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, True)
3028 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
3030 if not hasattr(self
, 'user_apps_reader'):
3031 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
3033 self
.comboAudioPlayerApp
.set_row_separator_func(self
.is_row_separator
)
3034 self
.comboVideoPlayerApp
.set_row_separator_func(self
.is_row_separator
)
3036 if gpodder
.interface
== gpodder
.GUI
:
3037 self
.user_apps_reader
.read()
3039 self
.comboAudioPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('audio'))
3040 index
= self
.find_active_audio_app()
3041 self
.comboAudioPlayerApp
.set_active(index
)
3042 self
.comboVideoPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('video'))
3043 index
= self
.find_active_video_app()
3044 self
.comboVideoPlayerApp
.set_active(index
)
3046 self
.ipodIcon
.set_from_icon_name( 'gnome-dev-ipod', gtk
.ICON_SIZE_BUTTON
)
3048 def is_row_separator(self
, model
, iter):
3049 return model
.get_value(iter, 0) == ''
3051 def update_mountpoint( self
, ipod
):
3052 if ipod
is None or ipod
.mount_point
is None:
3053 self
.iPodMountpoint
.set_label( '')
3055 self
.iPodMountpoint
.set_label( ipod
.mount_point
)
3057 def find_active_audio_app(self
):
3059 model
= self
.comboAudioPlayerApp
.get_model()
3060 iter = model
.get_iter_first()
3062 while iter is not None:
3063 command
= model
.get_value(iter, 1)
3064 if command
== self
.openApp
.get_text():
3066 if index_custom
< 0 and command
== '':
3067 index_custom
= index
3068 iter = model
.iter_next(iter)
3070 # return index of custom command or first item
3071 return max(0, index_custom
)
3073 def find_active_video_app( self
):
3075 model
= self
.comboVideoPlayerApp
.get_model()
3076 iter = model
.get_iter_first()
3078 while iter is not None:
3079 command
= model
.get_value(iter, 1)
3080 if command
== self
.openVideoApp
.get_text():
3082 if index_custom
< 0 and command
== '':
3083 index_custom
= index
3084 iter = model
.iter_next(iter)
3086 # return index of custom command or first item
3087 return max(0, index_custom
)
3089 def set_download_dir( self
, new_download_dir
, event
= None):
3090 gl
.downloaddir
= self
.chooserDownloadTo
.get_filename()
3091 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
3092 self
.notification(_('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
3097 def on_auto_update_feeds_toggled( self
, widget
, *args
):
3098 self
.auto_update_frequency
.set_sensitive(widget
.get_active())
3100 def on_display_tray_icon_toggled( self
, widget
, *args
):
3101 self
.enable_notifications
.set_sensitive(widget
.get_active())
3102 self
.minimize_to_tray
.set_sensitive(widget
.get_active())
3104 def on_cbCustomSyncName_toggled( self
, widget
, *args
):
3105 self
.entryCustomSyncName
.set_sensitive( widget
.get_active())
3107 def on_only_sync_not_played_toggled( self
, widget
, *args
):
3108 self
.delete_episodes_marked_played
.set_sensitive( widget
.get_active())
3109 if not widget
.get_active():
3110 self
.delete_episodes_marked_played
.set_active(False)
3112 def on_delete_episodes_marked_played_toggled( self
, widget
, *args
):
3113 if widget
.get_active() and self
.only_sync_not_played
.get_active():
3114 self
.on_sync_leave
.set_active(True)
3115 self
.on_sync_delete
.set_sensitive(not widget
.get_active())
3116 self
.on_sync_mark_played
.set_sensitive(not widget
.get_active())
3118 def on_btnCustomSyncNameHelp_clicked( self
, widget
):
3120 '<i>{episode.title}</i> -> <b>Interview with RMS</b>',
3121 '<i>{episode.basename}</i> -> <b>70908-interview-rms</b>',
3122 '<i>{episode.published}</i> -> <b>20070908</b> (for 08.09.2007)',
3123 '<i>{episode.pubtime}</i> -> <b>1344</b> (for 13:44)',
3124 '<i>{podcast.title}</i> -> <b>The Interview Podcast</b>'
3128 _('You can specify a custom format string for the file names on your MP3 player here.'),
3129 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3130 '\n'.join( [ ' %s' % s
for s
in examples
])
3133 self
.show_message( '\n\n'.join( info
), _('Custom format strings'))
3135 def on_gPodderProperties_destroy(self
, widget
, *args
):
3136 self
.on_btnOK_clicked( widget
, *args
)
3138 def on_btnConfigEditor_clicked(self
, widget
, *args
):
3139 self
.on_btnOK_clicked(widget
, *args
)
3140 gPodderConfigEditor()
3142 def on_comboAudioPlayerApp_changed(self
, widget
, *args
):
3143 # find out which one
3144 iter = self
.comboAudioPlayerApp
.get_active_iter()
3145 model
= self
.comboAudioPlayerApp
.get_model()
3146 command
= model
.get_value( iter, 1)
3148 if self
.openApp
.get_text() == 'default':
3149 self
.openApp
.set_text('')
3150 self
.openApp
.set_sensitive( True)
3152 self
.labelCustomCommand
.show()
3154 self
.openApp
.set_text( command
)
3155 self
.openApp
.set_sensitive( False)
3157 self
.labelCustomCommand
.hide()
3159 def on_comboVideoPlayerApp_changed(self
, widget
, *args
):
3160 # find out which one
3161 iter = self
.comboVideoPlayerApp
.get_active_iter()
3162 model
= self
.comboVideoPlayerApp
.get_model()
3163 command
= model
.get_value(iter, 1)
3165 if self
.openVideoApp
.get_text() == 'default':
3166 self
.openVideoApp
.set_text('')
3167 self
.openVideoApp
.set_sensitive(True)
3168 self
.openVideoApp
.show()
3169 self
.labelCustomVideoCommand
.show()
3171 self
.openVideoApp
.set_text(command
)
3172 self
.openVideoApp
.set_sensitive(False)
3173 self
.openVideoApp
.hide()
3174 self
.labelCustomVideoCommand
.hide()
3176 def on_cbEnvironmentVariables_toggled(self
, widget
, *args
):
3177 sens
= not self
.cbEnvironmentVariables
.get_active()
3178 self
.httpProxy
.set_sensitive( sens
)
3179 self
.ftpProxy
.set_sensitive( sens
)
3181 def on_comboboxDeviceType_changed(self
, widget
, *args
):
3182 active_item
= self
.comboboxDeviceType
.get_active()
3185 sync_widgets
= ( self
.only_sync_not_played
, self
.labelSyncOptions
,
3186 self
.imageSyncOptions
, self
. separatorSyncOptions
,
3187 self
.on_sync_mark_played
, self
.on_sync_delete
,
3188 self
.on_sync_leave
, self
.label_after_sync
, self
.delete_episodes_marked_played
)
3189 for widget
in sync_widgets
:
3190 if active_item
== 0:
3196 ipod_widgets
= (self
.ipodLabel
, self
.btn_iPodMountpoint
,
3197 self
.ipod_write_gtkpod_extended
)
3198 for widget
in ipod_widgets
:
3199 if active_item
== 1:
3204 # filesystem-based MP3 player
3205 fs_widgets
= ( self
.filesystemLabel
, self
.btn_filesystemMountpoint
,
3206 self
.cbChannelSubfolder
, self
.cbCustomSyncName
,
3207 self
.entryCustomSyncName
, self
.btnCustomSyncNameHelp
)
3208 for widget
in fs_widgets
:
3209 if active_item
== 2:
3214 def on_btn_iPodMountpoint_clicked(self
, widget
, *args
):
3215 fs
= gtk
.FileChooserDialog( title
= _('Select iPod mountpoint'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
3216 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3217 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3218 fs
.set_current_folder(self
.iPodMountpoint
.get_label())
3219 if fs
.run() == gtk
.RESPONSE_OK
:
3220 self
.iPodMountpoint
.set_label( fs
.get_filename())
3223 def on_btn_FilesystemMountpoint_clicked(self
, widget
, *args
):
3224 fs
= gtk
.FileChooserDialog( title
= _('Select folder for MP3 player'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
3225 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3226 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3227 fs
.set_current_folder(self
.filesystemMountpoint
.get_label())
3228 if fs
.run() == gtk
.RESPONSE_OK
:
3229 self
.filesystemMountpoint
.set_label( fs
.get_filename())
3232 def on_btnOK_clicked(self
, widget
, *args
):
3233 gl
.config
.ipod_mount
= self
.iPodMountpoint
.get_label()
3234 gl
.config
.mp3_player_folder
= self
.filesystemMountpoint
.get_label()
3236 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
3237 new_download_dir
= self
.chooserDownloadTo
.get_filename()
3238 download_dir_size
= util
.calculate_size( gl
.downloaddir
)
3239 download_dir_size_string
= gl
.format_filesize( download_dir_size
)
3242 dlg
= gtk
.Dialog( _('Moving downloads folder'), self
.gPodderProperties
)
3243 dlg
.vbox
.set_spacing( 5)
3244 dlg
.set_border_width( 5)
3247 label
.set_line_wrap( True)
3248 label
.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils
.escape( gl
.downloaddir
), saxutils
.escape( new_download_dir
), ))
3249 myprogressbar
= gtk
.ProgressBar()
3251 # put it all together
3252 dlg
.vbox
.pack_start( label
)
3253 dlg
.vbox
.pack_end( myprogressbar
)
3257 self
.gPodderProperties
.hide_all()
3259 # hide action area and separator line
3260 dlg
.action_area
.hide()
3261 dlg
.set_has_separator( False)
3263 args
= ( new_download_dir
, event
, )
3265 thread
= Thread( target
= self
.set_download_dir
, args
= args
)
3268 while not event
.isSet():
3270 new_download_dir_size
= util
.calculate_size( new_download_dir
)
3272 new_download_dir_size
= 0
3273 if download_dir_size
> 0:
3274 fract
= (1.00*new_download_dir_size
) / (1.00*download_dir_size
)
3278 myprogressbar
.set_text( _('%s of %s') % ( gl
.format_filesize( new_download_dir_size
), download_dir_size_string
, ))
3280 myprogressbar
.set_text( _('Finishing... please wait.'))
3281 myprogressbar
.set_fraction(max(0.0,min(1.0,fract
)))
3283 while gtk
.events_pending():
3284 gtk
.main_iteration( False)
3288 device_type
= self
.comboboxDeviceType
.get_active()
3289 if device_type
== 0:
3290 gl
.config
.device_type
= 'none'
3291 elif device_type
== 1:
3292 gl
.config
.device_type
= 'ipod'
3293 elif device_type
== 2:
3294 gl
.config
.device_type
= 'filesystem'
3295 elif device_type
== 3:
3296 gl
.config
.device_type
= 'mtp'
3297 self
.gPodderProperties
.destroy()
3298 if self
.callback_finished
:
3299 self
.callback_finished()
3302 class gPodderEpisode(GladeWidget
):
3303 finger_friendly_widgets
= ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3306 setattr(self
, 'episode', None)
3307 setattr(self
, 'download_callback', None)
3308 setattr(self
, 'play_callback', None)
3309 self
.gPodderEpisode
.connect('delete-event', self
.on_delete_event
)
3310 gl
.config
.connect_gtk_window(self
.gPodderEpisode
, 'episode_window', True)
3311 services
.download_status_manager
.register('list-changed', self
.on_download_status_changed
)
3312 services
.download_status_manager
.register('progress-detail', self
.on_download_status_progress
)
3313 self
.textview
.modify_bg(gtk
.STATE_NORMAL
, gtk
.gdk
.color_parse('#ffffff'))
3314 if gl
.config
.enable_html_shownotes
:
3317 setattr(self
, 'have_gtkhtml2', True)
3318 # Generate a HTML view and remove the textview
3319 setattr(self
, 'htmlview', gtkhtml2
.View())
3320 self
.scrolled_window
.remove(self
.scrolled_window
.get_child())
3321 self
.scrolled_window
.add(self
.htmlview
)
3322 self
.textview
= None
3323 self
.htmlview
.set_document(gtkhtml2
.Document())
3324 self
.htmlview
.show()
3326 log('Install gtkhtml2 if you want HTML shownotes', sender
=self
)
3327 setattr(self
, 'have_gtkhtml2', False)
3329 setattr(self
, 'have_gtkhtml2', False)
3331 def show(self
, episode
, download_callback
, play_callback
):
3332 self
.episode
= episode
3333 self
.download_callback
= download_callback
3334 self
.play_callback
= play_callback
3336 self
.gPodderEpisode
.set_title(self
.episode
.title
)
3338 if self
.have_gtkhtml2
:
3340 d
= gtkhtml2
.Document()
3341 d
.open_stream('text/html')
3342 d
.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3344 self
.htmlview
.set_document(d
)
3346 b
= gtk
.TextBuffer()
3347 self
.textview
.set_buffer(b
)
3349 self
.hide_show_widgets()
3350 self
.gPodderEpisode
.show()
3352 # Make sure the window comes up right now:
3353 while gtk
.events_pending():
3354 gtk
.main_iteration(False)
3356 # Now do the stuff that takes a bit longer...
3357 heading
= self
.episode
.title
3358 subheading
= 'from %s' % (self
.episode
.channel
.title
)
3359 description
= self
.episode
.description
3362 if self
.have_gtkhtml2
:
3364 d
.connect('link-clicked', lambda d
, url
: util
.open_website(url
))
3365 def request_url(document
, url
, stream
):
3366 def opendata(url
, stream
):
3367 fp
= urllib2
.urlopen(url
)
3368 data
= fp
.read(1024*10)
3371 data
= fp
.read(1024*10)
3373 Thread(target
=opendata
, args
=[url
, stream
]).start()
3374 d
.connect('request-url', request_url
)
3376 d
.open_stream('text/html')
3377 d
.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3378 d
.write_stream('<span style="font-size: big; font-weight: bold;">%s</span><br><span style="font-size: small;">%s</span><hr style="border: 1px #eeeeee solid;"><p>' % (saxutils
.escape(heading
), saxutils
.escape(subheading
)))
3379 d
.write_stream(self
.episode
.description
)
3381 d
.write_stream('<hr style="border: 1px #eeeeee solid;">')
3382 d
.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils
.escape(f
) for f
in footer
))),))
3383 d
.write_stream('</p></body></html>')
3386 b
.create_tag('heading', scale
=pango
.SCALE_LARGE
, weight
=pango
.WEIGHT_BOLD
)
3387 b
.create_tag('subheading', scale
=pango
.SCALE_SMALL
)
3388 b
.create_tag('footer', scale
=pango
.SCALE_SMALL
)
3390 b
.insert_with_tags_by_name(b
.get_end_iter(), heading
, 'heading')
3391 b
.insert_at_cursor('\n')
3392 b
.insert_with_tags_by_name(b
.get_end_iter(), subheading
, 'subheading')
3393 b
.insert_at_cursor('\n\n')
3394 b
.insert(b
.get_end_iter(), util
.remove_html_tags(description
))
3396 b
.insert_at_cursor('\n\n')
3397 b
.insert_with_tags_by_name(b
.get_end_iter(), '\n'.join(footer
), 'footer')
3399 services
.download_status_manager
.request_progress_detail(self
.episode
.url
)
3401 def on_cancel(self
, widget
):
3402 services
.download_status_manager
.cancel_by_url(self
.episode
.url
)
3404 def on_delete_event(self
, widget
, event
):
3405 # Avoid destroying the dialog, simply hide
3406 self
.on_close(widget
)
3409 def on_close(self
, widget
):
3411 if self
.have_gtkhtml2
:
3413 self
.htmlview
.set_document(gtkhtml2
.Document())
3415 self
.textview
.get_buffer().set_text('')
3416 self
.gPodderEpisode
.hide()
3418 def on_download_status_changed(self
, episode_urls
, channel_urls
):
3419 if self
.gPodderEpisode
.get_property('visible'):
3420 self
.hide_show_widgets()
3422 log('download status changed, but not visible', sender
=self
)
3424 def on_download_status_progress(self
, url
, progress
, speed
):
3425 if self
.episode
is None:
3428 if url
== self
.episode
.url
:
3429 progress
= float(min(100.0,max(0.0,progress
)))
3430 self
.download_progress
.set_fraction(progress
/100.0)
3431 self
.download_progress
.set_text('Downloading: %d%% (%s)' % (progress
, speed
))
3433 def hide_show_widgets(self
):
3434 is_downloading
= services
.download_status_manager
.is_download_in_progress(self
.episode
.url
)
3436 self
.download_progress
.show_all()
3437 self
.btnCancel
.show_all()
3438 self
.btnPlay
.hide_all()
3439 self
.btnDownload
.hide_all()
3441 self
.download_progress
.hide_all()
3442 self
.btnCancel
.hide_all()
3443 if os
.path
.exists(self
.episode
.local_filename()):
3444 if self
.episode
.file_type() in ('audio', 'video'):
3445 self
.btnPlay
.set_label(gtk
.STOCK_MEDIA_PLAY
)
3447 self
.btnPlay
.set_label(gtk
.STOCK_OPEN
)
3448 self
.btnPlay
.set_use_stock(True)
3449 self
.btnPlay
.show_all()
3450 self
.btnDownload
.hide_all()
3452 self
.btnPlay
.hide_all()
3453 self
.btnDownload
.show_all()
3455 def on_download(self
, widget
):
3456 if self
.download_callback
:
3457 self
.download_callback()
3459 def on_playback(self
, widget
):
3460 if self
.play_callback
:
3461 self
.play_callback()
3462 self
.on_close(widget
)
3464 class gPodderSync(GladeWidget
):
3466 util
.idle_add(self
.imageSync
.set_from_icon_name
, 'gnome-dev-ipod', gtk
.ICON_SIZE_DIALOG
)
3468 self
.device
.register('progress', self
.on_progress
)
3469 self
.device
.register('sub-progress', self
.on_sub_progress
)
3470 self
.device
.register('status', self
.on_status
)
3471 self
.device
.register('done', self
.on_done
)
3473 def on_progress(self
, pos
, max, text
=None):
3475 text
= _('%d of %d done') % (pos
, max)
3476 util
.idle_add(self
.progressbar
.set_fraction
, float(pos
)/float(max))
3477 util
.idle_add(self
.progressbar
.set_text
, text
)
3479 def on_sub_progress(self
, percentage
):
3480 util
.idle_add(self
.progressbar
.set_text
, _('Processing (%d%%)') % (percentage
))
3482 def on_status(self
, status
):
3483 util
.idle_add(self
.status_label
.set_markup
, '<i>%s</i>' % saxutils
.escape(status
))
3486 util
.idle_add(self
.gPodderSync
.destroy
)
3487 if not self
.gPodder
.minimized
:
3488 util
.idle_add(self
.notification
, _('Your device has been updated by gPodder.'), _('Operation finished'))
3490 def on_gPodderSync_destroy(self
, widget
, *args
):
3491 self
.device
.unregister('progress', self
.on_progress
)
3492 self
.device
.unregister('sub-progress', self
.on_sub_progress
)
3493 self
.device
.unregister('status', self
.on_status
)
3494 self
.device
.unregister('done', self
.on_done
)
3495 self
.device
.cancel()
3497 def on_cancel_button_clicked(self
, widget
, *args
):
3498 self
.device
.cancel()
3501 class gPodderOpmlLister(GladeWidget
):
3502 finger_friendly_widgets
= ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3505 # initiate channels list
3507 self
.callback_for_channel
= None
3508 self
.callback_finished
= None
3510 if hasattr(self
, 'custom_title'):
3511 self
.gPodderOpmlLister
.set_title(self
.custom_title
)
3512 if hasattr(self
, 'hide_url_entry'):
3513 self
.hboxOpmlUrlEntry
.hide_all()
3514 new_parent
= self
.notebookChannelAdder
.get_parent()
3515 new_parent
.remove(self
.notebookChannelAdder
)
3516 self
.vboxOpmlImport
.reparent(new_parent
)
3518 self
.setup_treeview(self
.treeviewChannelChooser
)
3519 self
.setup_treeview(self
.treeviewTopPodcastsChooser
)
3520 self
.setup_treeview(self
.treeviewYouTubeChooser
)
3522 self
.notebookChannelAdder
.connect('switch-page', lambda a
, b
, c
: self
.on_change_tab(c
))
3524 def setup_treeview(self
, tv
):
3525 togglecell
= gtk
.CellRendererToggle()
3526 togglecell
.set_property( 'activatable', True)
3527 togglecell
.connect( 'toggled', self
.callback_edited
)
3528 togglecolumn
= gtk
.TreeViewColumn( '', togglecell
, active
=0)
3530 titlecell
= gtk
.CellRendererText()
3531 titlecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
3532 titlecolumn
= gtk
.TreeViewColumn(_('Podcast'), titlecell
, markup
=1)
3534 for itemcolumn
in ( togglecolumn
, titlecolumn
):
3535 tv
.append_column(itemcolumn
)
3537 def callback_edited( self
, cell
, path
):
3538 model
= self
.get_treeview().get_model()
3540 url
= model
[path
][2]
3542 model
[path
][0] = not model
[path
][0]
3544 self
.channels
.append( url
)
3546 self
.channels
.remove( url
)
3548 self
.btnOK
.set_sensitive( bool(len(self
.get_selected_channels())))
3550 def get_selected_channels(self
, tab
=None):
3553 model
= self
.get_treeview(tab
).get_model()
3554 if model
is not None:
3557 channels
.append(row
[2])
3561 def on_change_tab(self
, tab
):
3562 self
.btnOK
.set_sensitive( bool(len(self
.get_selected_channels(tab
))))
3564 def thread_finished(self
, model
, tab
=0):
3566 tv
= self
.treeviewTopPodcastsChooser
3568 tv
= self
.treeviewYouTubeChooser
3569 self
.entryYoutubeSearch
.set_sensitive(True)
3570 self
.btnSearchYouTube
.set_sensitive(True)
3571 self
.btnOK
.set_sensitive(False)
3573 tv
= self
.treeviewChannelChooser
3574 self
.btnDownloadOpml
.set_sensitive(True)
3575 self
.entryURL
.set_sensitive(True)
3579 tv
.set_sensitive(True)
3581 def thread_func(self
, tab
=0):
3583 model
= opml
.Importer(gl
.config
.toplist_url
).get_model()
3585 self
.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3587 model
= resolver
.find_youtube_channels(self
.entryYoutubeSearch
.get_text())
3589 self
.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
3591 model
= opml
.Importer(self
.entryURL
.get_text()).get_model()
3593 self
.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3595 util
.idle_add(self
.thread_finished
, model
, tab
)
3597 def get_channels_from_url( self
, url
, callback_for_channel
= None, callback_finished
= None):
3598 if callback_for_channel
:
3599 self
.callback_for_channel
= callback_for_channel
3600 if callback_finished
:
3601 self
.callback_finished
= callback_finished
3602 self
.entryURL
.set_text( url
)
3603 self
.btnDownloadOpml
.set_sensitive( False)
3604 self
.entryURL
.set_sensitive( False)
3605 self
.btnOK
.set_sensitive( False)
3606 self
.treeviewChannelChooser
.set_sensitive( False)
3607 Thread( target
= self
.thread_func
).start()
3608 Thread( target
= lambda: self
.thread_func(1)).start()
3610 def select_all( self
, value
):
3612 model
= self
.get_treeview().get_model()
3613 if model
is not None:
3618 self
.btnOK
.set_sensitive(enabled
)
3620 def on_gPodderOpmlLister_destroy(self
, widget
, *args
):
3623 def on_btnDownloadOpml_clicked(self
, widget
, *args
):
3624 self
.get_channels_from_url( self
.entryURL
.get_text())
3626 def on_btnSearchYouTube_clicked(self
, widget
, *args
):
3627 self
.entryYoutubeSearch
.set_sensitive(False)
3628 self
.treeviewYouTubeChooser
.set_sensitive(False)
3629 self
.btnSearchYouTube
.set_sensitive(False)
3630 Thread(target
= lambda: self
.thread_func(2)).start()
3632 def on_btnSelectAll_clicked(self
, widget
, *args
):
3633 self
.select_all(True)
3635 def on_btnSelectNone_clicked(self
, widget
, *args
):
3636 self
.select_all(False)
3638 def on_btnOK_clicked(self
, widget
, *args
):
3639 self
.channels
= self
.get_selected_channels()
3640 self
.gPodderOpmlLister
.destroy()
3642 # add channels that have been selected
3643 for url
in self
.channels
:
3644 if self
.callback_for_channel
:
3645 self
.callback_for_channel( url
)
3647 if self
.callback_finished
:
3648 util
.idle_add(self
.callback_finished
)
3650 def on_btnCancel_clicked(self
, widget
, *args
):
3651 self
.gPodderOpmlLister
.destroy()
3653 def on_entryYoutubeSearch_key_press_event(self
, widget
, event
):
3654 if event
.keyval
== gtk
.keysyms
.Return
:
3655 self
.on_btnSearchYouTube_clicked(widget
)
3657 def get_treeview(self
, tab
=None):
3659 tab
= self
.notebookChannelAdder
.get_current_page()
3662 return self
.treeviewChannelChooser
3664 return self
.treeviewTopPodcastsChooser
3666 return self
.treeviewYouTubeChooser
3668 class gPodderEpisodeSelector( GladeWidget
):
3669 """Episode selection dialog
3671 Optional keyword arguments that modify the behaviour of this dialog:
3673 - callback: Function that takes 1 parameter which is a list of
3674 the selected episodes (or empty list when none selected)
3675 - remove_callback: Function that takes 1 parameter which is a list
3676 of episodes that should be "removed" (see below)
3677 (default is None, which means remove not possible)
3678 - remove_action: Label for the "remove" action (default is "Remove")
3679 - remove_finished: Callback after all remove callbacks have finished
3680 (default is None, also depends on remove_callback)
3681 It will get a list of episode URLs that have been
3682 removed, so the main UI can update those
3683 - episodes: List of episodes that are presented for selection
3684 - selected: (optional) List of boolean variables that define the
3685 default checked state for the given episodes
3686 - selected_default: (optional) The default boolean value for the
3687 checked state if no other value is set
3689 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3690 columns, the name is the attribute name of the episode to be
3691 read from each episode object. The sort name is the
3692 attribute name of the episode to be used to sort this column.
3693 If the sort_name is None it will use the attribute name for
3694 sorting. The sort type is the type of the sort column.
3695 The caption attribute is the text that appear as column caption
3696 (default is [('title_and_description', None, None, 'Episode'),])
3697 - title: (optional) The title of the window + heading
3698 - instructions: (optional) A one-line text describing what the
3699 user should select / what the selection is for
3700 - stock_ok_button: (optional) Will replace the "OK" button with
3701 another GTK+ stock item to be used for the
3702 affirmative button of the dialog (e.g. can
3703 be gtk.STOCK_DELETE when the episodes to be
3704 selected will be deleted after closing the
3706 - selection_buttons: (optional) A dictionary with labels as
3707 keys and callbacks as values; for each
3708 key a button will be generated, and when
3709 the button is clicked, the callback will
3710 be called for each episode and the return
3711 value of the callback (True or False) will
3712 be the new selected state of the episode
3713 - size_attribute: (optional) The name of an attribute of the
3714 supplied episode objects that can be used to
3715 calculate the size of an episode; set this to
3716 None if no total size calculation should be
3717 done (in cases where total size is useless)
3718 (default is 'length')
3719 - tooltip_attribute: (optional) The name of an attribute of
3720 the supplied episode objects that holds
3721 the text for the tooltips when hovering
3722 over an episode (default is 'description')
3725 finger_friendly_widgets
= ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3730 COLUMN_ADDITIONAL
= 3
3733 gl
.config
.connect_gtk_window(self
.gPodderEpisodeSelector
, 'episode_selector', True)
3734 if not hasattr( self
, 'callback'):
3735 self
.callback
= None
3737 if not hasattr(self
, 'remove_callback'):
3738 self
.remove_callback
= None
3740 if not hasattr(self
, 'remove_action'):
3741 self
.remove_action
= _('Remove')
3743 if not hasattr(self
, 'remove_finished'):
3744 self
.remove_finished
= None
3746 if not hasattr( self
, 'episodes'):
3749 if not hasattr( self
, 'size_attribute'):
3750 self
.size_attribute
= 'length'
3752 if not hasattr(self
, 'tooltip_attribute'):
3753 self
.tooltip_attribute
= 'description'
3755 if not hasattr( self
, 'selection_buttons'):
3756 self
.selection_buttons
= {}
3758 if not hasattr( self
, 'selected_default'):
3759 self
.selected_default
= False
3761 if not hasattr( self
, 'selected'):
3762 self
.selected
= [self
.selected_default
]*len(self
.episodes
)
3764 if len(self
.selected
) < len(self
.episodes
):
3765 self
.selected
+= [self
.selected_default
]*(len(self
.episodes
)-len(self
.selected
))
3767 if not hasattr( self
, 'columns'):
3768 self
.columns
= (('title_and_description', None, None, _('Episode')),)
3770 if hasattr( self
, 'title'):
3771 self
.gPodderEpisodeSelector
.set_title( self
.title
)
3772 self
.labelHeading
.set_markup( '<b><big>%s</big></b>' % saxutils
.escape( self
.title
))
3774 if gpodder
.interface
== gpodder
.MAEMO
:
3775 self
.labelHeading
.hide()
3777 if hasattr( self
, 'instructions'):
3778 self
.labelInstructions
.set_text( self
.instructions
)
3779 self
.labelInstructions
.show_all()
3781 if hasattr(self
, 'stock_ok_button'):
3782 if self
.stock_ok_button
== 'gpodder-download':
3783 self
.btnOK
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_BUTTON
))
3784 self
.btnOK
.set_label(_('Download'))
3786 self
.btnOK
.set_label(self
.stock_ok_button
)
3787 self
.btnOK
.set_use_stock(True)
3789 # check/uncheck column
3790 toggle_cell
= gtk
.CellRendererToggle()
3791 toggle_cell
.connect( 'toggled', self
.toggle_cell_handler
)
3792 self
.treeviewEpisodes
.append_column( gtk
.TreeViewColumn( '', toggle_cell
, active
=self
.COLUMN_TOGGLE
))
3794 next_column
= self
.COLUMN_ADDITIONAL
3795 for name
, sort_name
, sort_type
, caption
in self
.columns
:
3796 renderer
= gtk
.CellRendererText()
3797 renderer
.set_property( 'ellipsize', pango
.ELLIPSIZE_END
)
3798 column
= gtk
.TreeViewColumn(caption
, renderer
, markup
=next_column
)
3799 column
.set_resizable( True)
3800 # Only set "expand" on the first column (so more text is displayed there)
3801 column
.set_expand(next_column
== self
.COLUMN_ADDITIONAL
)
3802 if sort_name
is not None:
3803 column
.set_sort_column_id(next_column
+1)
3805 column
.set_sort_column_id(next_column
)
3806 self
.treeviewEpisodes
.append_column( column
)
3809 if sort_name
is not None:
3810 # add the sort column
3811 column
= gtk
.TreeViewColumn()
3812 column
.set_visible(False)
3813 self
.treeviewEpisodes
.append_column( column
)
3816 column_types
= [ gobject
.TYPE_INT
, gobject
.TYPE_STRING
, gobject
.TYPE_BOOLEAN
]
3817 # add string column type plus sort column type if it exists
3818 for name
, sort_name
, sort_type
, caption
in self
.columns
:
3819 column_types
.append(gobject
.TYPE_STRING
)
3820 if sort_name
is not None:
3821 column_types
.append(sort_type
)
3822 self
.model
= gtk
.ListStore( *column_types
)
3825 for index
, episode
in enumerate( self
.episodes
):
3826 if self
.tooltip_attribute
is not None:
3828 tooltip
= getattr(episode
, self
.tooltip_attribute
)
3830 log('Episode object %s does not have tooltip attribute: "%s"', episode
, self
.tooltip_attribute
, sender
=self
)
3832 row
= [ index
, tooltip
, self
.selected
[index
] ]
3833 for name
, sort_name
, sort_type
, caption
in self
.columns
:
3834 if not hasattr(episode
, name
):
3835 log('Warning: Missing attribute "%s"', name
, sender
=self
)
3838 row
.append(getattr( episode
, name
))
3840 if sort_name
is not None:
3841 if not hasattr(episode
, sort_name
):
3842 log('Warning: Missing attribute "%s"', sort_name
, sender
=self
)
3845 row
.append(getattr( episode
, sort_name
))
3846 self
.model
.append( row
)
3848 if self
.remove_callback
is not None:
3849 self
.btnRemoveAction
.show()
3850 self
.btnRemoveAction
.set_label(self
.remove_action
)
3852 # connect to tooltip signals
3853 if self
.tooltip_attribute
is not None:
3855 self
.treeviewEpisodes
.set_property('has-tooltip', True)
3856 self
.treeviewEpisodes
.connect('query-tooltip', self
.treeview_episodes_query_tooltip
)
3858 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender
=self
)
3859 self
.last_tooltip_episode
= None
3860 self
.episode_list_can_tooltip
= True
3862 self
.treeviewEpisodes
.connect('button-press-event', self
.treeview_episodes_button_pressed
)
3863 self
.treeviewEpisodes
.set_rules_hint( True)
3864 self
.treeviewEpisodes
.set_model( self
.model
)
3865 self
.treeviewEpisodes
.columns_autosize()
3866 self
.calculate_total_size()
3868 def treeview_episodes_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
3869 # With get_bin_window, we get the window that contains the rows without
3870 # the header. The Y coordinate of this window will be the height of the
3871 # treeview header. This is the amount we have to subtract from the
3872 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3873 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
3876 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos(x
, y
) or (None,)*4
3878 if not self
.episode_list_can_tooltip
:
3879 self
.last_tooltip_episode
= None
3882 if path
is not None:
3883 model
= treeview
.get_model()
3884 iter = model
.get_iter(path
)
3885 index
= model
.get_value(iter, self
.COLUMN_INDEX
)
3886 description
= model
.get_value(iter, self
.COLUMN_TOOLTIP
)
3887 if self
.last_tooltip_episode
is not None and self
.last_tooltip_episode
!= index
:
3888 self
.last_tooltip_episode
= None
3890 self
.last_tooltip_episode
= index
3892 if description
is not None:
3893 tooltip
.set_text(description
)
3898 self
.last_tooltip_episode
= None
3901 def treeview_episodes_button_pressed(self
, treeview
, event
):
3902 if event
.button
== 3:
3905 if len(self
.selection_buttons
):
3906 for label
in self
.selection_buttons
:
3907 item
= gtk
.MenuItem(label
)
3908 item
.connect('activate', self
.custom_selection_button_clicked
, label
)
3910 menu
.append(gtk
.SeparatorMenuItem())
3912 item
= gtk
.MenuItem(_('Select all'))
3913 item
.connect('activate', self
.on_btnCheckAll_clicked
)
3916 item
= gtk
.MenuItem(_('Select none'))
3917 item
.connect('activate', self
.on_btnCheckNone_clicked
)
3921 # Disable tooltips while we are showing the menu, so
3922 # the tooltip will not appear over the menu
3923 self
.episode_list_can_tooltip
= False
3924 menu
.connect('deactivate', lambda menushell
: self
.episode_list_allow_tooltips())
3925 menu
.popup(None, None, None, event
.button
, event
.time
)
3929 def episode_list_allow_tooltips(self
):
3930 self
.episode_list_can_tooltip
= True
3932 def calculate_total_size( self
):
3933 if self
.size_attribute
is not None:
3934 (total_size
, count
) = (0, 0)
3935 for episode
in self
.get_selected_episodes():
3937 total_size
+= int(getattr( episode
, self
.size_attribute
))
3940 log( 'Cannot get size for %s', episode
.title
, sender
= self
)
3944 text
.append(_('Nothing selected'))
3946 text
.append(_('One episode selected'))
3948 text
.append(_('%d episodes selected') % count
)
3950 text
.append(_('total size: %s') % gl
.format_filesize(total_size
))
3951 self
.labelTotalSize
.set_text(', '.join(text
))
3952 self
.btnOK
.set_sensitive(count
>0)
3953 self
.btnRemoveAction
.set_sensitive(count
>0)
3955 self
.btnCancel
.set_label(gtk
.STOCK_CANCEL
)
3957 self
.btnCancel
.set_label(gtk
.STOCK_CLOSE
)
3959 self
.btnOK
.set_sensitive(False)
3960 self
.btnRemoveAction
.set_sensitive(False)
3961 for index
, row
in enumerate(self
.model
):
3962 if self
.model
.get_value(row
.iter, self
.COLUMN_TOGGLE
) == True:
3963 self
.btnOK
.set_sensitive(True)
3964 self
.btnRemoveAction
.set_sensitive(True)
3966 self
.labelTotalSize
.set_text('')
3968 def toggle_cell_handler( self
, cell
, path
):
3969 model
= self
.treeviewEpisodes
.get_model()
3970 model
[path
][self
.COLUMN_TOGGLE
] = not model
[path
][self
.COLUMN_TOGGLE
]
3972 self
.calculate_total_size()
3974 def custom_selection_button_clicked(self
, button
, label
):
3975 callback
= self
.selection_buttons
[label
]
3977 for index
, row
in enumerate( self
.model
):
3978 new_value
= callback( self
.episodes
[index
])
3979 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, new_value
)
3981 self
.calculate_total_size()
3983 def on_btnCheckAll_clicked( self
, widget
):
3984 for row
in self
.model
:
3985 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, True)
3987 self
.calculate_total_size()
3989 def on_btnCheckNone_clicked( self
, widget
):
3990 for row
in self
.model
:
3991 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, False)
3993 self
.calculate_total_size()
3995 def on_remove_action_activate(self
, widget
):
3996 episodes
= self
.get_selected_episodes(remove_episodes
=True)
3999 for episode
in episodes
:
4000 urls
.append(episode
.url
)
4001 self
.remove_callback(episode
)
4003 if self
.remove_finished
is not None:
4004 self
.remove_finished(urls
)
4005 self
.calculate_total_size()
4007 def get_selected_episodes( self
, remove_episodes
=False):
4008 selected_episodes
= []
4010 for index
, row
in enumerate( self
.model
):
4011 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
4012 selected_episodes
.append( self
.episodes
[self
.model
.get_value( row
.iter, self
.COLUMN_INDEX
)])
4015 for episode
in selected_episodes
:
4016 index
= self
.episodes
.index(episode
)
4017 iter = self
.model
.get_iter_first()
4018 while iter is not None:
4019 if self
.model
.get_value(iter, self
.COLUMN_INDEX
) == index
:
4020 self
.model
.remove(iter)
4022 iter = self
.model
.iter_next(iter)
4024 return selected_episodes
4026 def on_btnOK_clicked( self
, widget
):
4027 self
.gPodderEpisodeSelector
.destroy()
4028 if self
.callback
is not None:
4029 self
.callback( self
.get_selected_episodes())
4031 def on_btnCancel_clicked( self
, widget
):
4032 self
.gPodderEpisodeSelector
.destroy()
4033 if self
.callback
is not None:
4036 class gPodderConfigEditor(GladeWidget
):
4037 finger_friendly_widgets
= ['btnShowAll', 'btnClose', 'configeditor']
4040 name_column
= gtk
.TreeViewColumn(_('Setting'))
4041 name_renderer
= gtk
.CellRendererText()
4042 name_column
.pack_start(name_renderer
)
4043 name_column
.add_attribute(name_renderer
, 'text', 0)
4044 name_column
.add_attribute(name_renderer
, 'style', 5)
4045 self
.configeditor
.append_column(name_column
)
4047 value_column
= gtk
.TreeViewColumn(_('Set to'))
4048 value_check_renderer
= gtk
.CellRendererToggle()
4049 value_column
.pack_start(value_check_renderer
, expand
=False)
4050 value_column
.add_attribute(value_check_renderer
, 'active', 7)
4051 value_column
.add_attribute(value_check_renderer
, 'visible', 6)
4052 value_column
.add_attribute(value_check_renderer
, 'activatable', 6)
4053 value_check_renderer
.connect('toggled', self
.value_toggled
)
4055 value_renderer
= gtk
.CellRendererText()
4056 value_column
.pack_start(value_renderer
)
4057 value_column
.add_attribute(value_renderer
, 'text', 2)
4058 value_column
.add_attribute(value_renderer
, 'visible', 4)
4059 value_column
.add_attribute(value_renderer
, 'editable', 4)
4060 value_column
.add_attribute(value_renderer
, 'style', 5)
4061 value_renderer
.connect('edited', self
.value_edited
)
4062 self
.configeditor
.append_column(value_column
)
4064 self
.model
= gl
.config
.model()
4065 self
.filter = self
.model
.filter_new()
4066 self
.filter.set_visible_func(self
.visible_func
)
4068 self
.configeditor
.set_model(self
.filter)
4069 self
.configeditor
.set_rules_hint(True)
4071 def visible_func(self
, model
, iter, user_data
=None):
4072 text
= self
.entryFilter
.get_text().lower()
4076 # either the variable name or its value
4077 return (text
in model
.get_value(iter, 0).lower() or
4078 text
in model
.get_value(iter, 2).lower())
4080 def value_edited(self
, renderer
, path
, new_text
):
4081 model
= self
.configeditor
.get_model()
4082 iter = model
.get_iter(path
)
4083 name
= model
.get_value(iter, 0)
4084 type_cute
= model
.get_value(iter, 1)
4086 if not gl
.config
.update_field(name
, new_text
):
4087 self
.notification(_('Cannot set value of <b>%s</b> to <i>%s</i>.\n\nNeeded data type: %s') % (saxutils
.escape(name
), saxutils
.escape(new_text
), saxutils
.escape(type_cute
)), _('Error updating %s') % saxutils
.escape(name
))
4089 def value_toggled(self
, renderer
, path
):
4090 model
= self
.configeditor
.get_model()
4091 iter = model
.get_iter(path
)
4092 field_name
= model
.get_value(iter, 0)
4093 field_type
= model
.get_value(iter, 3)
4095 # Flip the boolean config flag
4096 if field_type
== bool:
4097 gl
.config
.toggle_flag(field_name
)
4099 def on_entryFilter_changed(self
, widget
):
4100 self
.filter.refilter()
4102 def on_btnShowAll_clicked(self
, widget
):
4103 self
.entryFilter
.set_text('')
4104 self
.entryFilter
.grab_focus()
4106 def on_btnClose_clicked(self
, widget
):
4107 self
.gPodderConfigEditor
.destroy()
4109 class gPodderPlaylist(GladeWidget
):
4110 finger_friendly_widgets
= ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4113 self
.m3u_header
= '#EXTM3U\n'
4114 self
.mountpoint
= util
.find_mount_point(gl
.config
.mp3_player_folder
)
4115 if self
.mountpoint
== '/':
4116 self
.mountpoint
= gl
.config
.mp3_player_folder
4117 log('Warning: MP3 player resides on / - using %s as MP3 player root', self
.mountpoint
, sender
=self
)
4118 self
.playlist_file
= os
.path
.join(self
.mountpoint
,
4119 gl
.config
.mp3_player_playlist_file
)
4120 icon_theme
= gtk
.icon_theme_get_default()
4121 self
.icon_new
= icon_theme
.load_icon(gtk
.STOCK_NEW
, 16, 0)
4124 check_cell
= gtk
.CellRendererToggle()
4125 check_cell
.set_property('activatable', True)
4126 check_cell
.connect('toggled', self
.cell_toggled
)
4127 check_column
= gtk
.TreeViewColumn(_('Use'), check_cell
, active
=1)
4128 self
.treeviewPlaylist
.append_column(check_column
)
4131 column
= gtk
.TreeViewColumn(_('Filename'))
4132 icon_cell
= gtk
.CellRendererPixbuf()
4133 column
.pack_start(icon_cell
, False)
4134 column
.add_attribute(icon_cell
, 'pixbuf', 0)
4135 filename_cell
= gtk
.CellRendererText()
4136 column
.pack_start(filename_cell
, True)
4137 column
.add_attribute(filename_cell
, 'text', 2)
4139 column
.set_resizable(True)
4140 self
.treeviewPlaylist
.append_column(column
)
4142 # Make treeview reorderable
4143 self
.treeviewPlaylist
.set_reorderable(True)
4146 self
.playlist
= gtk
.ListStore(gtk
.gdk
.Pixbuf
, bool, str)
4147 self
.treeviewPlaylist
.set_model(self
.playlist
)
4149 # read device and playlist and fill the TreeView
4150 self
.m3u
= self
.read_m3u()
4151 self
.device
= self
.read_device()
4154 def cell_toggled(self
, cellrenderertoggle
, path
):
4155 (treeview
, liststore
) = (self
.treeviewPlaylist
, self
.playlist
)
4156 it
= liststore
.get_iter(path
)
4157 liststore
.set_value(it
, 1, not liststore
.get_value(it
, 1))
4159 def on_btnCancelPlaylist_clicked(self
, widget
):
4160 self
.gPodderPlaylist
.destroy()
4162 def on_btnSavePlaylist_clicked(self
, widget
):
4164 self
.gPodderPlaylist
.destroy()
4168 read all files from the existing playlist
4171 if os
.path
.exists(self
.playlist_file
):
4172 for line
in open(self
.playlist_file
, 'r'):
4173 if line
!= self
.m3u_header
:
4174 if line
.startswith('#'):
4175 tracks
.append([False, line
[1:].strip()])
4177 tracks
.append([True, line
.strip()])
4180 def write_m3u(self
):
4182 write the list into the playlist on the device
4184 playlist_folder
= os
.path
.split(self
.playlist_file
)[0]
4185 if not util
.make_directory(playlist_folder
):
4186 self
.show_message(_('Folder %s could not be created.') % playlist_folder
, _('Error writing playlist'))
4189 fp
= open(self
.playlist_file
, 'w')
4190 fp
.write(self
.m3u_header
)
4191 for icon
, checked
, filename
in self
.playlist
:
4197 self
.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4198 except IOError, ioe
:
4199 self
.show_message(str(ioe
), _('Error writing playlist file'))
4201 def read_device(self
):
4203 read all files from the device
4206 for root
, dirs
, files
in os
.walk(gl
.config
.mp3_player_folder
):
4208 filename
= os
.path
.join(root
, file)
4210 if filename
== self
.playlist_file
:
4211 # We don't want to have our playlist file as
4212 # an entry in our file list, so skip it!
4215 if not gl
.config
.mp3_player_playlist_absolute_path
:
4216 filename
= filename
[len(self
.mountpoint
):]
4218 if gl
.config
.mp3_player_playlist_win_path
:
4219 filename
= filename
.replace( '/', '\\')
4221 tracks
.append(filename
)
4224 def write2gui(self
):
4225 # add the files from the device to the list only when
4226 # they are not yet in the playlist
4227 # mark this files as NEW
4228 for filename
in self
.device
[:]:
4229 m3ulist
= [file[1] for file in self
.m3u
]
4230 if filename
not in m3ulist
:
4231 self
.playlist
.append([self
.icon_new
, False, filename
])
4233 # add the files from the playlist to the list only when
4234 # they are on the device
4235 for checked
, filename
in self
.m3u
[:]:
4236 if filename
in self
.device
:
4237 self
.playlist
.append([None, checked
, filename
])
4239 class gPodderDependencyManager(GladeWidget
):
4241 col_name
= gtk
.TreeViewColumn(_('Feature'), gtk
.CellRendererText(), text
=0)
4242 self
.treeview_components
.append_column(col_name
)
4243 col_installed
= gtk
.TreeViewColumn(_('Status'), gtk
.CellRendererText(), text
=2)
4244 self
.treeview_components
.append_column(col_installed
)
4245 self
.treeview_components
.set_model(services
.dependency_manager
.get_model())
4246 self
.btn_about
.set_sensitive(False)
4248 def on_btn_about_clicked(self
, widget
):
4249 selection
= self
.treeview_components
.get_selection()
4250 model
, iter = selection
.get_selected()
4251 if iter is not None:
4252 title
= model
.get_value(iter, 0)
4253 description
= model
.get_value(iter, 1)
4254 available
= model
.get_value(iter, 3)
4255 missing
= model
.get_value(iter, 4)
4258 description
+= '\n\n'+_('Missing components:')+'\n\n'+missing
4260 self
.show_message(description
, title
)
4262 def on_btn_install_clicked(self
, widget
):
4263 # TODO: Implement package manager integration
4266 def on_treeview_components_cursor_changed(self
, treeview
):
4267 self
.btn_about
.set_sensitive(treeview
.get_selection().count_selected_rows() > 0)
4268 # TODO: If installing is possible, enable btn_install
4270 def on_gPodderDependencyManager_response(self
, dialog
, response_id
):
4271 self
.gPodderDependencyManager
.destroy()
4273 class gPodderWelcome(GladeWidget
):
4274 finger_friendly_widgets
= ['btnOPML', 'btnMygPodder', 'btnCancel']
4279 def on_show_example_podcasts(self
, button
):
4280 self
.gPodderWelcome
.destroy()
4281 self
.show_example_podcasts_callback(None)
4283 def on_setup_my_gpodder(self
, gpodder
):
4284 self
.gPodderWelcome
.destroy()
4285 self
.setup_my_gpodder_callback(None)
4287 def on_btnCancel_clicked(self
, button
):
4288 self
.gPodderWelcome
.destroy()
4291 gobject
.threads_init()
4292 gtk
.window_set_default_icon_name( 'gpodder')