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/>.
33 from xml
.sax
import saxutils
35 from threading
import Event
36 from threading
import Thread
37 from string
import strip
40 from gpodder
import util
41 from gpodder
import opml
42 from gpodder
import services
43 from gpodder
import sync
44 from gpodder
import download
45 from gpodder
import SimpleGladeApp
46 from gpodder
.liblogger
import log
47 from gpodder
.dbsqlite
import db
50 from gpodder
import trayicon
52 except Exception, exc
:
53 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
54 log('Warning: This probably means your PyGTK installation is too old!')
57 from libpodcasts
import podcastChannel
58 from libpodcasts
import LocalDBReader
59 from libpodcasts
import podcastItem
60 from libpodcasts
import channels_to_model
61 from libpodcasts
import load_channels
62 from libpodcasts
import update_channels
63 from libpodcasts
import save_channels
64 from libpodcasts
import can_restore_from_opml
66 from gpodder
.libgpodder
import gl
68 from libplayers
import UserAppsReader
70 from libtagupdate
import tagging_supported
72 if gpodder
.interface
== gpodder
.GUI
:
73 WEB_BROWSER_ICON
= 'web-browser'
74 elif gpodder
.interface
== gpodder
.MAEMO
:
76 WEB_BROWSER_ICON
= 'qgn_toolb_browser_web'
79 app_version
= "unknown" # will be set in main() call
81 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
83 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
84 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andy Busch',
85 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen',
86 'Bernd Schlapsi', 'Bill Barnard', 'Bjørn Rasmussen', 'Camille Moncelier',
87 'Carlos Moffat', 'Chris', 'Chris Arnold', 'Clark Burbidge', 'Daniel Ramos',
88 'David Spreen', 'Doug Hellmann', 'FFranci72', 'Florian Richter', 'Frank Harper',
89 'FriedBunny', 'Gerrit Sangel', 'Götz Waschk',
90 'Haim Roitgrund', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
91 'Jérôme Chabod', 'Jerry Moss',
92 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
93 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
94 'Jürgen Schinker', 'Justin Forest',
95 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Michael Salim',
96 'Mika Leppinen', 'Mike Coulson', 'Mykola Nikishov', 'narf at inode.at',
97 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
98 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
99 'Pavel Mlčoch', 'Peter Hoffmann', 'Philippe Gouaillier', 'Pieter de Decker',
100 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
101 'Robert Young', 'Roel Groeneveld',
102 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'SPGoetze',
103 'Stefan Lohmaier', 'Stephan Buys', 'Stylianos Papanastasiou', 'Teo Ramirez',
104 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
105 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'VladDrac',
106 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
108 'List may be incomplete - please contact me.'
110 app_copyright
= '© 2005-2008 Thomas Perl and the gPodder Team'
111 app_website
= 'http://www.gpodder.org/'
113 # these will be filled with pathnames in bin/gpodder
114 glade_dir
= [ 'share', 'gpodder' ]
115 icon_dir
= [ 'share', 'pixmaps', 'gpodder.png' ]
116 scalable_dir
= [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
119 class GladeWidget(SimpleGladeApp
.SimpleGladeApp
):
120 gpodder_main_window
= None
121 finger_friendly_widgets
= []
123 def __init__( self
, **kwargs
):
124 path
= os
.path
.join( glade_dir
, '%s.glade' % app_name
)
125 root
= self
.__class
__.__name
__
128 SimpleGladeApp
.SimpleGladeApp
.__init
__( self
, path
, root
, domain
, **kwargs
)
130 # Set widgets to finger-friendly mode if on Maemo
131 for widget_name
in self
.finger_friendly_widgets
:
132 self
.set_finger_friendly(getattr(self
, widget_name
))
134 if root
== 'gPodder':
135 GladeWidget
.gpodder_main_window
= self
.gPodder
137 # If we have a child window, set it transient for our main window
138 getattr( self
, root
).set_transient_for( GladeWidget
.gpodder_main_window
)
140 if gpodder
.interface
== gpodder
.GUI
:
141 if hasattr( self
, 'center_on_widget'):
142 ( x
, y
) = self
.gpodder_main_window
.get_position()
143 a
= self
.center_on_widget
.allocation
144 ( x
, y
) = ( x
+ a
.x
, y
+ a
.y
)
145 ( w
, h
) = ( a
.width
, a
.height
)
146 ( pw
, ph
) = getattr( self
, root
).get_size()
147 getattr( self
, root
).move( x
+ w
/2 - pw
/2, y
+ h
/2 - ph
/2)
149 getattr( self
, root
).set_position( gtk
.WIN_POS_CENTER_ON_PARENT
)
151 def notification(self
, message
, title
=None):
152 util
.idle_add(self
.show_message
, message
, title
)
154 def show_message( self
, message
, title
= None):
155 if hasattr(self
, 'tray_icon') and hasattr(self
, 'minimized') and self
.tray_icon
and self
.minimized
:
158 self
.tray_icon
.send_notification(message
, title
)
161 if gpodder
.interface
== gpodder
.GUI
:
162 dlg
= gtk
.MessageDialog(GladeWidget
.gpodder_main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
)
164 dlg
.set_title(str(title
))
165 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
167 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
168 elif gpodder
.interface
== gpodder
.MAEMO
:
169 dlg
= hildon
.Note('information', (GladeWidget
.gpodder_main_window
, message
))
174 def set_finger_friendly(self
, widget
):
176 If we are on Maemo, we carry out the necessary
177 operations to turn a widget into a finger-friendly
178 one, depending on which type of widget it is (i.e.
179 buttons will have more padding, TreeViews a thick
182 if gpodder
.interface
== gpodder
.MAEMO
:
183 if isinstance(widget
, gtk
.Misc
):
184 widget
.set_padding(0, 5)
185 elif isinstance(widget
, gtk
.Button
):
186 for child
in widget
.get_children():
187 if isinstance(child
, gtk
.Alignment
):
188 child
.set_padding(10, 10, 5, 5)
190 child
.set_padding(10, 10)
191 elif isinstance(widget
, gtk
.TreeView
) or isinstance(widget
, gtk
.TextView
):
192 parent
= widget
.get_parent()
193 if isinstance(parent
, gtk
.ScrolledWindow
):
194 hildon
.hildon_helper_set_thumb_scrollbar(parent
, True)
195 elif isinstance(widget
, gtk
.MenuItem
):
196 for child
in widget
.get_children():
197 self
.set_finger_friendly(child
)
199 log('Cannot set widget finger-friendly: %s', widget
, sender
=self
)
203 def show_confirmation( self
, message
, title
= None):
204 if gpodder
.interface
== gpodder
.GUI
:
205 affirmative
= gtk
.RESPONSE_YES
206 dlg
= gtk
.MessageDialog(GladeWidget
.gpodder_main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
)
208 dlg
.set_title(str(title
))
209 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
211 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
212 elif gpodder
.interface
== gpodder
.MAEMO
:
213 affirmative
= gtk
.RESPONSE_OK
214 dlg
= hildon
.Note('confirmation', (GladeWidget
.gpodder_main_window
, message
))
219 return response
== affirmative
221 def show_copy_dialog( self
, src_filename
, dst_filename
= None, dst_directory
= None, title
= _('Select destination')):
222 if dst_filename
is None:
223 dst_filename
= src_filename
225 if dst_directory
is None:
226 dst_directory
= os
.path
.expanduser( '~')
228 ( base
, extension
) = os
.path
.splitext( src_filename
)
230 if not dst_filename
.endswith( extension
):
231 dst_filename
+= extension
233 if gpodder
.interface
== gpodder
.GUI
:
234 dlg
= gtk
.FileChooserDialog(title
=title
, parent
=GladeWidget
.gpodder_main_window
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
235 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
236 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
237 elif gpodder
.interface
== gpodder
.MAEMO
:
238 dlg
= hildon
.FileChooserDialog(GladeWidget
.gpodder_main_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
240 dlg
.set_do_overwrite_confirmation( True)
241 dlg
.set_current_name( os
.path
.basename( dst_filename
))
242 dlg
.set_current_folder( dst_directory
)
244 if dlg
.run() == gtk
.RESPONSE_OK
:
245 dst_filename
= dlg
.get_filename()
246 if not dst_filename
.endswith( extension
):
247 dst_filename
+= extension
249 log( 'Copying %s => %s', src_filename
, dst_filename
, sender
= self
)
252 shutil
.copyfile( src_filename
, dst_filename
)
254 log( 'Error copying file.', sender
= self
, traceback
= True)
260 class gPodder(GladeWidget
):
261 finger_friendly_widgets
= ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
262 ENTER_URL_TEXT
= _('Enter podcast URL...')
265 if gpodder
.interface
== gpodder
.MAEMO
:
266 # Maemo-specific changes to the UI
268 scalable_dir
= scalable_dir
.replace('.svg', '.png')
270 self
.app
= hildon
.Program()
271 gtk
.set_application_name('gPodder')
272 self
.window
= hildon
.Window()
273 self
.window
.connect('delete-event', self
.on_gPodder_delete_event
)
274 self
.window
.connect('window-state-event', self
.window_state_event
)
275 self
.window
.connect('key-press-event', self
.on_key_press
)
277 # Give toolbar to the hildon window
278 self
.toolbar
.parent
.remove(self
.toolbar
)
279 self
.toolbar
.set_style(gtk
.TOOLBAR_ICONS
)
280 self
.window
.add_toolbar(self
.toolbar
)
282 self
.app
.add_window(self
.window
)
283 self
.vMain
.reparent(self
.window
)
284 self
.gPodder
= self
.window
286 # Reparent the main menu
288 for child
in self
.mainMenu
.get_children():
290 self
.itemQuit
.reparent(menu
)
291 self
.window
.set_menu(menu
)
293 self
.mainMenu
.destroy()
296 # do some widget hiding
297 self
.toolbar
.remove(self
.toolTransfer
)
298 self
.itemTransferSelected
.hide_all()
299 self
.item_show_url_entry
.hide_all()
300 self
.item_email_subscriptions
.hide_all()
302 # Feed cache update button
303 self
.label120
.set_text(_('Update'))
305 # get screen real estate
306 self
.hboxContainer
.set_border_width(0)
308 self
.treeChannels
.connect('size-allocate', self
.on_tree_channels_resize
)
310 if gpodder
.interface
== gpodder
.MAEMO
or not gl
.config
.show_podcast_url_entry
:
311 self
.hboxAddChannel
.hide_all()
313 if not gl
.config
.show_toolbar
:
314 self
.toolbar
.hide_all()
316 gl
.config
.add_observer(self
.on_config_changed
)
317 self
.default_entry_text_color
= self
.entryAddChannel
.get_style().text
[gtk
.STATE_NORMAL
]
318 self
.entryAddChannel
.connect('focus-in-event', self
.entry_add_channel_focus
)
319 self
.entryAddChannel
.connect('focus-out-event', self
.entry_add_channel_unfocus
)
320 self
.entry_add_channel_unfocus(self
.entryAddChannel
, None)
323 self
.tray_icon
= None
325 self
.fullscreen
= False
326 self
.minimized
= False
327 self
.gPodder
.connect('window-state-event', self
.window_state_event
)
329 self
.already_notified_new_episodes
= []
330 self
.show_hide_tray_icon()
331 self
.no_episode_selected
.set_sensitive(False)
333 self
.itemShowToolbar
.set_active(gl
.config
.show_toolbar
)
334 self
.itemShowDescription
.set_active(gl
.config
.episode_list_descriptions
)
335 self
.item_show_url_entry
.set_active(gl
.config
.show_podcast_url_entry
)
337 gl
.config
.connect_gtk_window( self
.gPodder
)
338 gl
.config
.connect_gtk_paned( 'paned_position', self
.channelPaned
)
340 gl
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
341 gl
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
342 gl
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
343 gl
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
345 # Make sure we free/close the download queue when we
346 # update the "max downloads" spin button
347 changed_cb
= lambda spinbutton
: services
.download_status_manager
.update_max_downloads()
348 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
350 self
.default_title
= None
351 if app_version
.rfind('svn') != -1:
352 self
.set_title('gPodder %s' % app_version
)
354 self
.set_title(self
.gPodder
.get_title())
356 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
358 # cell renderers for channel tree
359 namecolumn
= gtk
.TreeViewColumn( _('Podcast'))
361 iconcell
= gtk
.CellRendererPixbuf()
362 namecolumn
.pack_start( iconcell
, False)
363 namecolumn
.add_attribute( iconcell
, 'pixbuf', 5)
364 self
.cell_channel_icon
= iconcell
366 namecell
= gtk
.CellRendererText()
367 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
368 namecolumn
.pack_start( namecell
, True)
369 namecolumn
.add_attribute( namecell
, 'markup', 2)
371 iconcell
= gtk
.CellRendererPixbuf()
372 iconcell
.set_property('xalign', 1.0)
373 namecolumn
.pack_start( iconcell
, False)
374 namecolumn
.add_attribute( iconcell
, 'pixbuf', 3)
375 namecolumn
.add_attribute(iconcell
, 'visible', 7)
376 self
.cell_channel_pill
= iconcell
378 self
.treeChannels
.append_column( namecolumn
)
379 self
.treeChannels
.set_headers_visible(False)
381 # enable alternating colors hint
382 self
.treeAvailable
.set_rules_hint( True)
383 self
.treeChannels
.set_rules_hint( True)
385 # connect to tooltip signals
387 self
.treeChannels
.set_property('has-tooltip', True)
388 self
.treeChannels
.connect('query-tooltip', self
.treeview_channels_query_tooltip
)
389 self
.treeAvailable
.set_property('has-tooltip', True)
390 self
.treeAvailable
.connect('query-tooltip', self
.treeview_episodes_query_tooltip
)
392 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender
= self
)
393 self
.last_tooltip_channel
= None
394 self
.last_tooltip_episode
= None
395 self
.podcast_list_can_tooltip
= True
396 self
.episode_list_can_tooltip
= True
398 # Add our context menu to treeAvailable
399 if gpodder
.interface
== gpodder
.MAEMO
:
400 self
.treeAvailable
.connect('button-release-event', self
.treeview_button_pressed
)
402 self
.treeAvailable
.connect('button-press-event', self
.treeview_button_pressed
)
403 self
.treeChannels
.connect('button-press-event', self
.treeview_channels_button_pressed
)
405 iconcell
= gtk
.CellRendererPixbuf()
406 if gpodder
.interface
== gpodder
.MAEMO
:
407 status_column_label
= ''
409 status_column_label
= _('Status')
410 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=4)
412 namecell
= gtk
.CellRendererText()
413 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
414 namecolumn
= gtk
.TreeViewColumn(_("Episode"), namecell
, markup
=6)
415 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
416 namecolumn
.set_expand(True)
418 sizecell
= gtk
.CellRendererText()
419 sizecolumn
= gtk
.TreeViewColumn( _("Size"), sizecell
, text
=2)
421 releasecell
= gtk
.CellRendererText()
422 releasecolumn
= gtk
.TreeViewColumn( _("Released"), releasecell
, text
=5)
424 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
425 itemcolumn
.set_reorderable(True)
426 self
.treeAvailable
.append_column(itemcolumn
)
428 if gpodder
.interface
== gpodder
.MAEMO
:
429 # Due to screen space contraints, we
430 # hide these columns here by default
431 self
.column_size
= sizecolumn
432 self
.column_released
= releasecolumn
433 self
.column_released
.set_visible(False)
434 self
.column_size
.set_visible(False)
436 # enable search in treeavailable
437 self
.treeAvailable
.set_search_equal_func( self
.treeAvailable_search_equal
)
439 # enable multiple selection support
440 self
.treeAvailable
.get_selection().set_mode( gtk
.SELECTION_MULTIPLE
)
441 self
.treeDownloads
.get_selection().set_mode( gtk
.SELECTION_MULTIPLE
)
443 # columns and renderers for "download progress" tab
444 episodecell
= gtk
.CellRendererText()
445 episodecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
446 episodecolumn
= gtk
.TreeViewColumn( _("Episode"), episodecell
, text
=0)
447 episodecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
448 episodecolumn
.set_expand(True)
450 speedcell
= gtk
.CellRendererText()
451 speedcolumn
= gtk
.TreeViewColumn( _("Speed"), speedcell
, text
=1)
453 progresscell
= gtk
.CellRendererProgress()
454 progresscolumn
= gtk
.TreeViewColumn( _("Progress"), progresscell
, value
=2)
455 progresscolumn
.set_expand(True)
457 for itemcolumn
in ( episodecolumn
, speedcolumn
, progresscolumn
):
458 self
.treeDownloads
.append_column( itemcolumn
)
460 # After we've set up most of the window, show it :)
461 if not gpodder
.interface
== gpodder
.MAEMO
:
465 if gl
.config
.start_iconified
:
466 self
.iconify_main_window()
467 elif gl
.config
.minimize_to_tray
:
468 self
.tray_icon
.set_visible(False)
470 services
.download_status_manager
.register( 'list-changed', self
.download_status_updated
)
471 services
.download_status_manager
.register( 'progress-changed', self
.download_progress_updated
)
472 services
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
473 services
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
474 self
.cover_cache
= {}
476 self
.treeDownloads
.set_model( services
.download_status_manager
.tree_model
)
478 #Add Drag and Drop Support
479 flags
= gtk
.DEST_DEFAULT_ALL
480 targets
= [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
481 actions
= gtk
.gdk
.ACTION_DEFAULT | gtk
.gdk
.ACTION_COPY
482 self
.treeChannels
.drag_dest_set( flags
, targets
, actions
)
483 self
.treeChannels
.connect( 'drag_data_received', self
.drag_data_received
)
485 # Subscribed channels
486 self
.active_channel
= None
487 self
.channels
= load_channels()
489 if len(self
.channels
):
490 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
492 self
.label2
.set_text(_('Podcasts'))
494 # load list of user applications for audio playback
495 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
496 Thread(target
=self
.read_apps
).start()
498 # Clean up old, orphaned download files
499 gl
.clean_up_downloads( delete_partial
= True)
501 # Set the "Device" menu item for the first time
502 self
.update_item_device()
504 # Now, update the feed cache, when everything's in place
505 self
.feed_cache_update_cancelled
= False
506 self
.update_feed_cache(force_update
=gl
.config
.update_on_startup
)
508 # Start the auto-update procedure
509 self
.auto_update_procedure(first_run
=True)
511 # Delete old episodes if the user wishes to
512 if gl
.config
.auto_remove_old_episodes
:
513 old_episodes
= self
.get_old_episodes()
514 if len(old_episodes
) > 0:
515 self
.delete_episode_list(old_episodes
, confirm
=False)
516 self
.updateComboBox()
518 # First-time users should be asked if they want to see the OPML
519 if len(self
.channels
) == 0:
520 util
.idle_add(self
.on_itemUpdate_activate
, None)
522 def on_tree_channels_resize(self
, widget
, allocation
):
523 if not gl
.config
.podcast_sidebar_save_space
:
526 window_allocation
= self
.gPodder
.get_allocation()
527 percentage
= 100. * float(allocation
.width
) / float(window_allocation
.width
)
528 if hasattr(self
, 'cell_channel_icon'):
529 self
.cell_channel_icon
.set_property('visible', bool(percentage
> 22.))
530 if hasattr(self
, 'cell_channel_pill'):
531 self
.cell_channel_pill
.set_property('visible', bool(percentage
> 25.))
533 def entry_add_channel_focus(self
, widget
, event
):
534 widget
.modify_text(gtk
.STATE_NORMAL
, self
.default_entry_text_color
)
535 if widget
.get_text() == self
.ENTER_URL_TEXT
:
538 def entry_add_channel_unfocus(self
, widget
, event
):
539 if widget
.get_text() == '':
540 widget
.set_text(self
.ENTER_URL_TEXT
)
541 widget
.modify_text(gtk
.STATE_NORMAL
, gtk
.gdk
.color_parse('#aaaaaa'))
543 def on_config_changed(self
, name
, old_value
, new_value
):
544 if name
== 'show_toolbar':
546 self
.toolbar
.show_all()
548 self
.toolbar
.hide_all()
549 elif name
== 'episode_list_descriptions':
550 for channel
in self
.channels
:
551 channel
.force_update_tree_model()
552 self
.updateTreeView()
553 elif name
== 'show_podcast_url_entry' and gpodder
.interface
!= gpodder
.MAEMO
:
555 self
.hboxAddChannel
.show_all()
557 self
.hboxAddChannel
.hide_all()
560 time
.sleep(3) # give other parts of gpodder a chance to start up
561 self
.user_apps_reader
.read()
562 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
563 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
565 def treeview_episodes_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
566 # With get_bin_window, we get the window that contains the rows without
567 # the header. The Y coordinate of this window will be the height of the
568 # treeview header. This is the amount we have to subtract from the
569 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
570 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
573 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
575 if not self
.episode_list_can_tooltip
:
576 self
.last_tooltip_episode
= None
580 model
= treeview
.get_model()
581 iter = model
.get_iter(path
)
582 url
= model
.get_value(iter, 0)
583 description
= model
.get_value(iter, 7)
584 if self
.last_tooltip_episode
is not None and self
.last_tooltip_episode
!= url
:
585 self
.last_tooltip_episode
= None
587 self
.last_tooltip_episode
= url
589 tooltip
.set_text(description
)
592 self
.last_tooltip_episode
= None
595 def podcast_list_allow_tooltips(self
):
596 self
.podcast_list_can_tooltip
= True
598 def episode_list_allow_tooltips(self
):
599 self
.episode_list_can_tooltip
= True
601 def treeview_channels_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
602 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
604 if not self
.podcast_list_can_tooltip
:
605 self
.last_tooltip_channel
= None
609 model
= treeview
.get_model()
610 iter = model
.get_iter(path
)
611 url
= model
.get_value(iter, 0)
612 for channel
in self
.channels
:
613 if channel
.url
== url
:
614 if self
.last_tooltip_channel
is not None and self
.last_tooltip_channel
!= channel
:
615 self
.last_tooltip_channel
= None
617 self
.last_tooltip_channel
= channel
618 channel
.request_save_dir_size()
619 diskspace_str
= gl
.format_filesize(channel
.save_dir_size
, 0)
620 error_str
= model
.get_value(iter, 6)
622 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
623 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
624 table
= gtk
.Table(rows
=3, columns
=3)
625 table
.set_row_spacings(5)
626 table
.set_col_spacings(5)
627 table
.set_border_width(5)
629 heading
= gtk
.Label()
630 heading
.set_alignment(0, 1)
631 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
632 table
.attach(heading
, 0, 1, 0, 1)
633 size_info
= gtk
.Label()
634 size_info
.set_alignment(1, 1)
635 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
636 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
637 table
.attach(size_info
, 2, 3, 0, 1)
639 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
641 if len(channel
.description
) < 500:
642 description
= channel
.description
644 pos
= channel
.description
.find('\n\n')
645 if pos
== -1 or pos
> 500:
646 description
= channel
.description
[:498]+'[...]'
648 description
= channel
.description
[:pos
]
650 description
= gtk
.Label(description
)
652 description
.set_markup(error_str
)
653 description
.set_alignment(0, 0)
654 description
.set_line_wrap(True)
655 table
.attach(description
, 0, 3, 2, 3)
658 tooltip
.set_custom(table
)
662 self
.last_tooltip_channel
= None
665 def update_m3u_playlist_clicked(self
, widget
):
666 self
.active_channel
.update_m3u_playlist()
667 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
669 def treeview_channels_button_pressed( self
, treeview
, event
):
670 global WEB_BROWSER_ICON
672 if event
.button
== 3:
673 ( x
, y
) = ( int(event
.x
), int(event
.y
) )
674 ( path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
678 # Did the user right-click into a selection?
679 selection
= treeview
.get_selection()
680 if selection
.count_selected_rows() and path
:
681 ( model
, paths
) = selection
.get_selected_rows()
682 if path
not in paths
:
683 # We have right-clicked, but not into the
684 # selection, assume we don't want to operate
688 # No selection or right click not in selection:
689 # Select the single item where we clicked
690 if not len( paths
) and path
:
691 treeview
.grab_focus()
692 treeview
.set_cursor( path
, column
, 0)
694 ( model
, paths
) = ( treeview
.get_model(), [ path
] )
696 # We did not find a selection, and the user didn't
697 # click on an item to select -- don't show the menu
703 item
= gtk
.ImageMenuItem( _('Open download folder'))
704 item
.set_image( gtk
.image_new_from_icon_name( 'folder-open', gtk
.ICON_SIZE_MENU
))
705 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
708 if gl
.config
.create_m3u_playlists
:
709 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
710 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
711 item
.connect('activate', self
.update_m3u_playlist_clicked
)
714 if self
.active_channel
.link
:
715 item
= gtk
.ImageMenuItem(_('Visit website'))
716 item
.set_image(gtk
.image_new_from_icon_name(WEB_BROWSER_ICON
, gtk
.ICON_SIZE_MENU
))
717 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
720 menu
.append( gtk
.SeparatorMenuItem())
722 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
723 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
726 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
727 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
731 # Disable tooltips while we are showing the menu, so
732 # the tooltip will not appear over the menu
733 self
.podcast_list_can_tooltip
= False
734 menu
.connect('deactivate', lambda menushell
: self
.podcast_list_allow_tooltips())
735 menu
.popup( None, None, None, event
.button
, event
.time
)
739 def on_itemClose_activate(self
, widget
):
740 if self
.tray_icon
is not None:
741 if gpodder
.interface
== gpodder
.MAEMO
:
742 self
.gPodder
.set_property('visible', False)
744 self
.iconify_main_window()
746 self
.on_gPodder_delete_event(widget
)
748 def cover_file_removed(self
, channel_url
):
750 The Cover Downloader calls this when a previously-
751 available cover has been removed from the disk. We
752 have to update our cache to reflect this change.
754 (COLUMN_URL
, COLUMN_PIXBUF
) = (0, 5)
755 for row
in self
.treeChannels
.get_model():
756 if row
[COLUMN_URL
] == channel_url
:
757 row
[COLUMN_PIXBUF
] = None
758 key
= (channel_url
, gl
.config
.podcast_list_icon_size
, \
759 gl
.config
.podcast_list_icon_size
)
760 if key
in self
.cover_cache
:
761 del self
.cover_cache
[key
]
764 def cover_download_finished(self
, channel_url
, pixbuf
):
766 The Cover Downloader calls this when it has finished
767 downloading (or registering, if already downloaded)
768 a new channel cover, which is ready for displaying.
770 if pixbuf
is not None:
771 (COLUMN_URL
, COLUMN_PIXBUF
) = (0, 5)
772 for row
in self
.treeChannels
.get_model():
773 if row
[COLUMN_URL
] == channel_url
and row
[COLUMN_PIXBUF
] is None:
774 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
)
775 row
[COLUMN_PIXBUF
] = new_pixbuf
or pixbuf
777 def save_episode_as_file( self
, url
, *args
):
778 episode
= self
.active_channel
.find_episode( url
)
780 self
.show_copy_dialog( src_filename
= episode
.local_filename(), dst_filename
= episode
.sync_filename())
782 def copy_episode_bluetooth(self
, url
, *args
):
783 episode
= self
.active_channel
.find_episode(url
)
784 filename
= episode
.local_filename()
786 if gl
.config
.bluetooth_ask_always
:
789 device
= gl
.config
.bluetooth_device_address
791 destfile
= os
.path
.join(gl
.tempdir
, episode
.sync_filename())
792 (base
, ext
) = os
.path
.splitext(filename
)
793 if not destfile
.endswith(ext
):
796 if gl
.config
.bluetooth_use_converter
:
797 title
= _('Converting file')
798 message
= _('Please wait while gPodder converts your media file for bluetooth file transfer.')
799 dlg
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_NONE
)
801 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
806 def convert_and_send_thread(filename
, destfile
, device
, dialog
, notify
):
807 if gl
.config
.bluetooth_use_converter
:
808 p
= subprocess
.Popen([gl
.config
.bluetooth_converter
, filename
, destfile
], stdout
=sys
.stdout
, stderr
=sys
.stderr
)
810 if dialog
is not None:
814 shutil
.copyfile(filename
, destfile
)
817 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
820 if result
== 0 or not os
.path
.exists(destfile
):
821 util
.bluetooth_send_file(destfile
, device
)
823 notify(_('Error converting file.'), _('Bluetooth file transfer'))
824 util
.delete_file(destfile
)
826 Thread(target
=convert_and_send_thread
, args
=[filename
, destfile
, device
, dlg
, self
.notification
]).start()
828 def treeview_button_pressed( self
, treeview
, event
):
829 global WEB_BROWSER_ICON
831 # Use right-click for the Desktop version and left-click for Maemo
832 if (event
.button
== 1 and gpodder
.interface
== gpodder
.MAEMO
) or \
833 (event
.button
== 3 and gpodder
.interface
== gpodder
.GUI
):
834 ( x
, y
) = ( int(event
.x
), int(event
.y
) )
835 ( path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
839 # Did the user right-click into a selection?
840 selection
= self
.treeAvailable
.get_selection()
841 if selection
.count_selected_rows() and path
:
842 ( model
, paths
) = selection
.get_selected_rows()
843 if path
not in paths
:
844 # We have right-clicked, but not into the
845 # selection, assume we don't want to operate
849 # No selection or right click not in selection:
850 # Select the single item where we clicked
851 if not len( paths
) and path
:
852 treeview
.grab_focus()
853 treeview
.set_cursor( path
, column
, 0)
855 ( model
, paths
) = ( treeview
.get_model(), [ path
] )
857 # We did not find a selection, and the user didn't
858 # click on an item to select -- don't show the menu
862 first_url
= model
.get_value( model
.get_iter( paths
[0]), 0)
863 episode
= db
.load_episode(first_url
)
867 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = self
.play_or_download()
870 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
871 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.toolPlay
))
872 menu
.append(self
.set_finger_friendly(item
))
874 if not episode
['is_locked'] and can_delete
:
875 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
876 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
877 menu
.append(self
.set_finger_friendly(item
))
880 item
= gtk
.ImageMenuItem( _('Cancel download'))
881 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_STOP
, gtk
.ICON_SIZE_MENU
))
882 item
.connect( 'activate', lambda w
: self
.on_treeDownloads_row_activated( self
.toolCancel
))
883 menu
.append(self
.set_finger_friendly(item
))
886 item
= gtk
.ImageMenuItem(_('Download'))
887 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
888 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.toolDownload
))
889 menu
.append(self
.set_finger_friendly(item
))
891 if episode
['state'] == db
.STATE_NORMAL
and not episode
['is_played']: # can_download:
892 item
= gtk
.ImageMenuItem(_('Do not download'))
893 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
894 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
895 menu
.append(self
.set_finger_friendly(item
))
896 elif episode
['state'] != db
.STATE_NORMAL
and can_download
:
897 item
= gtk
.ImageMenuItem(_('Mark as new'))
898 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
899 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
900 menu
.append(self
.set_finger_friendly(item
))
903 menu
.append( gtk
.SeparatorMenuItem())
904 item
= gtk
.ImageMenuItem(_('Save to disk'))
905 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
906 item
.connect( 'activate', lambda w
: self
.save_episode_as_file( episode_url
))
907 menu
.append(self
.set_finger_friendly(item
))
908 if gl
.config
.bluetooth_enabled
:
909 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
910 item
.set_image(gtk
.image_new_from_icon_name('bluetooth', gtk
.ICON_SIZE_MENU
))
911 item
.connect('activate', lambda w
: self
.copy_episode_bluetooth(episode_url
))
912 menu
.append(self
.set_finger_friendly(item
))
914 item
= gtk
.ImageMenuItem(_('Transfer to %s') % gl
.get_device_name())
915 item
.set_image(gtk
.image_new_from_icon_name('multimedia-player', gtk
.ICON_SIZE_MENU
))
916 item
.connect('activate', lambda w
: self
.on_treeAvailable_row_activated(self
.toolTransfer
))
917 menu
.append(self
.set_finger_friendly(item
))
920 menu
.append( gtk
.SeparatorMenuItem())
921 is_played
= episode
['is_played']
923 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
924 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
925 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
926 menu
.append(self
.set_finger_friendly(item
))
928 item
= gtk
.ImageMenuItem(_('Mark as played'))
929 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
930 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
931 menu
.append(self
.set_finger_friendly(item
))
933 is_locked
= episode
['is_locked']
935 item
= gtk
.ImageMenuItem(_('Allow deletion'))
936 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
937 item
.connect('activate', self
.on_item_toggle_lock_activate
)
938 menu
.append(self
.set_finger_friendly(item
))
940 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
941 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
942 item
.connect('activate', self
.on_item_toggle_lock_activate
)
943 menu
.append(self
.set_finger_friendly(item
))
946 menu
.append(gtk
.SeparatorMenuItem())
947 # Single item, add episode information menu item
948 episode_url
= model
.get_value( model
.get_iter( paths
[0]), 0)
949 item
= gtk
.ImageMenuItem(_('Episode details'))
950 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
951 item
.connect( 'activate', lambda w
: self
.on_treeAvailable_row_activated( self
.treeAvailable
))
952 menu
.append(self
.set_finger_friendly(item
))
953 episode
= self
.active_channel
.find_episode(episode_url
)
954 # If we have it, also add episode website link
955 if episode
and episode
.link
and episode
.link
!= episode
.url
:
956 item
= gtk
.ImageMenuItem(_('Visit website'))
957 item
.set_image(gtk
.image_new_from_icon_name(WEB_BROWSER_ICON
, gtk
.ICON_SIZE_MENU
))
958 item
.connect('activate', lambda w
: util
.open_website(episode
.link
))
959 menu
.append(self
.set_finger_friendly(item
))
961 if gpodder
.interface
== gpodder
.MAEMO
:
962 # Because we open the popup on left-click for Maemo,
963 # we also include a non-action to close the menu
964 menu
.append(gtk
.SeparatorMenuItem())
965 item
= gtk
.ImageMenuItem(_('Close this menu'))
966 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
967 menu
.append(self
.set_finger_friendly(item
))
970 # Disable tooltips while we are showing the menu, so
971 # the tooltip will not appear over the menu
972 self
.episode_list_can_tooltip
= False
973 menu
.connect('deactivate', lambda menushell
: self
.episode_list_allow_tooltips())
974 menu
.popup( None, None, None, event
.button
, event
.time
)
978 def set_title(self
, new_title
):
979 self
.default_title
= new_title
980 self
.gPodder
.set_title(new_title
)
982 def download_progress_updated( self
, count
, percentage
):
983 title
= [ self
.default_title
]
986 title
.append( _('downloading one file'))
988 title
.append( _('downloading %d files') % count
)
991 title
[1] = ''.join( [ title
[1], ' (%d%%)' % ( percentage
, ) ])
993 self
.gPodder
.set_title( ' - '.join( title
))
995 # Have all the downloads completed?
996 # If so execute user command if defined, else do nothing
998 if len(gl
.config
.cmd_all_downloads_complete
) > 0:
999 Thread(target
=gl
.ext_command_thread
, args
=(self
.notification
,gl
.config
.cmd_all_downloads_complete
)).start()
1001 def playback_episode( self
, current_channel
, current_podcast
):
1002 (success
, application
) = gl
.playback_episode(current_channel
, current_podcast
)
1004 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
), ))
1005 self
.download_status_updated()
1007 def treeAvailable_search_equal( self
, model
, column
, key
, iter, data
= None):
1013 # columns, as defined in libpodcasts' get model method
1014 # 1 = episode title, 7 = description
1017 for column
in columns
:
1018 value
= model
.get_value( iter, column
).lower()
1019 if value
.find( key
) != -1:
1024 def change_menu_item(self
, menuitem
, icon
=None, label
=None):
1025 (label_widget
, icon_widget
) = menuitem
.get_children()
1026 if icon
is not None:
1027 icon_widget
.set_from_icon_name(icon
, gtk
.ICON_SIZE_MENU
)
1028 if label
is not None:
1029 label_widget
.set_text(label
)
1031 def play_or_download(self
):
1032 if self
.wNotebook
.get_current_page() > 0:
1035 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1036 ( is_played
, is_locked
) = (False,)*2
1038 selection
= self
.treeAvailable
.get_selection()
1039 if selection
.count_selected_rows() > 0:
1040 (model
, paths
) = selection
.get_selected_rows()
1043 url
= model
.get_value( model
.get_iter( path
), 0)
1044 local_filename
= model
.get_value( model
.get_iter( path
), 8)
1046 episode
= podcastItem
.load(url
, self
.active_channel
)
1048 if episode
.was_downloaded(and_exists
=True):
1051 is_played
= episode
.is_played
1052 is_locked
= episode
.is_locked
1054 if services
.download_status_manager
.is_download_in_progress(url
):
1059 if self
.active_channel
.find_episode(url
).file_type() == 'torrent':
1060 can_download
= can_download
or gl
.config
.use_gnome_bittorrent
1062 can_download
= can_download
and not can_cancel
1063 can_play
= can_play
and not can_cancel
and not can_download
1064 can_transfer
= can_play
and gl
.config
.device_type
!= 'none'
1066 self
.toolPlay
.set_sensitive( can_play
)
1067 self
.toolDownload
.set_sensitive( can_download
)
1068 self
.toolTransfer
.set_sensitive( can_transfer
)
1069 self
.toolCancel
.set_sensitive( can_cancel
)
1072 self
.item_cancel_download
.show_all()
1074 self
.item_cancel_download
.hide_all()
1076 self
.itemDownloadSelected
.show_all()
1078 self
.itemDownloadSelected
.hide_all()
1080 self
.itemPlaySelected
.show_all()
1081 self
.itemDeleteSelected
.show_all()
1082 self
.item_toggle_played
.show_all()
1083 self
.item_toggle_lock
.show_all()
1084 self
.separator9
.show_all()
1086 self
.change_menu_item(self
.item_toggle_played
, gtk
.STOCK_CANCEL
, _('Mark as unplayed'))
1088 self
.change_menu_item(self
.item_toggle_played
, gtk
.STOCK_APPLY
, _('Mark as played'))
1090 self
.change_menu_item(self
.item_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Allow deletion'))
1092 self
.change_menu_item(self
.item_toggle_lock
, gtk
.STOCK_DIALOG_AUTHENTICATION
, _('Prohibit deletion'))
1094 self
.itemPlaySelected
.hide_all()
1095 self
.itemDeleteSelected
.hide_all()
1096 self
.item_toggle_played
.hide_all()
1097 self
.item_toggle_lock
.hide_all()
1098 self
.separator9
.hide_all()
1099 if can_play
or can_download
or can_cancel
:
1100 self
.item_episode_details
.show_all()
1101 self
.separator16
.show_all()
1102 self
.no_episode_selected
.hide_all()
1104 self
.item_episode_details
.hide_all()
1105 self
.separator16
.hide_all()
1106 self
.no_episode_selected
.show_all()
1108 return ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
)
1110 def download_status_updated( self
):
1111 count
= services
.download_status_manager
.count()
1113 self
.labelDownloads
.set_text( _('Downloads (%d)') % count
)
1115 self
.labelDownloads
.set_text( _('Downloads'))
1117 for channel
in self
.channels
:
1118 channel
.update_model()
1120 self
.updateComboBox()
1122 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1123 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1125 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1126 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1128 def updateComboBox(self
, selected_url
=None):
1129 (model
, iter) = self
.treeChannels
.get_selection().get_selected()
1131 if model
and iter and selected_url
is None:
1132 # Get the URL of the currently-selected podcast
1133 selected_url
= model
.get_value(iter, 0)
1135 rect
= self
.treeChannels
.get_visible_rect()
1136 self
.treeChannels
.set_model(channels_to_model(self
.channels
, self
.cover_cache
, gl
.config
.podcast_list_icon_size
, gl
.config
.podcast_list_icon_size
))
1137 util
.idle_add(self
.treeChannels
.scroll_to_point
, rect
.x
, rect
.y
)
1139 for channel
in self
.channels
:
1140 services
.cover_downloader
.request_cover(channel
)
1143 selected_path
= (0,)
1144 # Find the previously-selected URL in the new
1145 # model if we have an URL (else select first)
1146 if selected_url
is not None:
1147 model
= self
.treeChannels
.get_model()
1148 pos
= model
.get_iter_first()
1149 while pos
is not None:
1150 url
= model
.get_value(pos
, 0)
1151 if url
== selected_url
:
1152 selected_path
= model
.get_path(pos
)
1154 pos
= model
.iter_next(pos
)
1156 self
.treeChannels
.get_selection().select_path(selected_path
)
1158 log( 'Cannot set selection on treeChannels', sender
= self
)
1159 self
.on_treeChannels_cursor_changed( self
.treeChannels
)
1161 def updateTreeView( self
):
1162 if self
.channels
and self
.active_channel
is not None:
1163 self
.treeAvailable
.set_model(self
.active_channel
.tree_model
)
1164 self
.treeAvailable
.columns_autosize()
1165 self
.play_or_download()
1167 if self
.treeAvailable
.get_model():
1168 self
.treeAvailable
.get_model().clear()
1170 def drag_data_received(self
, widget
, context
, x
, y
, sel
, ttype
, time
):
1171 (path
, column
, rx
, ry
) = self
.treeChannels
.get_path_at_pos( x
, y
) or (None,)*4
1174 if path
is not None:
1175 model
= self
.treeChannels
.get_model()
1176 iter = model
.get_iter(path
)
1177 url
= model
.get_value(iter, 0)
1178 for channel
in self
.channels
:
1179 if channel
.url
== url
:
1180 dnd_channel
= channel
1184 rl
= result
.strip().lower()
1185 if (rl
.endswith('.jpg') or rl
.endswith('.png') or rl
.endswith('.gif') or rl
.endswith('.svg')) and dnd_channel
is not None:
1186 services
.cover_downloader
.replace_cover(dnd_channel
, result
)
1188 self
.add_new_channel(result
)
1190 def add_new_channel(self
, result
=None, ask_download_new
=True, quiet
=False):
1191 result
= util
.normalize_feed_url( result
)
1194 for old_channel
in self
.channels
:
1195 if old_channel
.url
== result
:
1196 log( 'Channel already exists: %s', result
)
1197 # Select the existing channel in combo box
1198 for i
in range( len( self
.channels
)):
1199 if self
.channels
[i
] == old_channel
:
1200 self
.treeChannels
.get_selection().select_path( (i
,))
1201 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1203 self
.show_message( _('You have already subscribed to this podcast: %s') % ( saxutils
.escape( old_channel
.title
), ), _('Already added'))
1205 log( 'Adding new channel: %s', result
)
1207 channel
= podcastChannel
.load(url
=result
, create
=True)
1208 except Exception, e
:
1209 log('Error in podcastChannel.load(%s): %s', result
, e
, backtrace
=True, sender
=self
)
1212 if channel
is not None:
1213 self
.channels
.append( channel
)
1214 save_channels( self
.channels
)
1216 # download changed channels and select the new episode in the UI afterwards
1217 self
.update_feed_cache(force_update
=False, select_url_afterwards
=channel
.url
)
1219 (username
, password
) = util
.username_password_from_url( result
)
1220 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')):
1221 channel
.username
= username
1222 channel
.password
= password
1223 log('Saving authentication data for episode downloads..', sender
= self
)
1226 if ask_download_new
:
1227 new_episodes
= channel
.get_new_episodes()
1228 if len(new_episodes
):
1229 self
.new_episodes_show(new_episodes
)
1231 # Ok, the URL is not a channel, or there is some other
1232 # error - let's see if it's a web page or OPML file...
1234 data
= urllib2
.urlopen(result
).read().lower()
1235 if '</opml>' in data
:
1236 # This looks like an OPML feed
1237 self
.on_item_import_from_file_activate(None, result
)
1239 elif '</html>' in data
:
1240 # This looks like a web page
1241 title
= _('The URL is a website')
1242 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.)')
1243 if self
.show_confirmation(message
, title
):
1244 util
.open_website(result
)
1246 except Exception, e
:
1247 log('Error trying to handle the URL as OPML or web page: %s', e
, sender
=self
)
1249 title
= _('Error adding podcast')
1250 message
= _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1251 self
.show_message( message
, title
)
1254 title
= _('URL scheme not supported')
1255 message
= _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1256 self
.show_message( message
, title
)
1258 self
.show_message(_('There has been an error adding this podcast. Please see the log output for more information.'), _('Error adding podcast'))
1260 def update_feed_cache_callback(self
, progressbar
, position
, count
, force_update
):
1261 if position
< len(self
.channels
):
1262 title
= self
.channels
[position
].title
1264 progression
= _('Updating %s (%d/%d)')%(title
, position
+1, count
)
1266 progression
= _('Loading %s (%d/%d)')%(title
, position
+1, count
)
1267 progressbar
.set_text(progression
)
1269 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
1272 progressbar
.set_fraction(float(position
)/float(count
))
1274 def update_feed_cache_finish_callback(self
, force_update
=False, notify_no_new_episodes
=False, select_url_afterwards
=None):
1275 self
.hboxUpdateFeeds
.hide_all()
1276 self
.btnUpdateFeeds
.show_all()
1278 # If we want to select a specific podcast (via its URL)
1279 # after the update, we give it to updateComboBox here to
1280 # select exactly this podcast after updating the view
1281 self
.updateComboBox(selected_url
=select_url_afterwards
)
1284 self
.tray_icon
.set_status(None)
1285 if self
.minimized
and force_update
:
1287 # look for new episodes to notify
1288 for channel
in self
.channels
:
1289 for episode
in channel
.get_new_episodes():
1290 if not episode
in self
.already_notified_new_episodes
:
1291 new_episodes
.append(episode
)
1292 self
.already_notified_new_episodes
.append(episode
)
1293 # notify new episodes
1295 if len(new_episodes
) == 0:
1296 if notify_no_new_episodes
and self
.tray_icon
is not None:
1297 msg
= _('No new episodes available for download')
1298 self
.tray_icon
.send_notification(msg
)
1300 elif len(new_episodes
) == 1:
1301 title
= _('gPodder has found %s') % (_('one new episode:'),)
1303 title
= _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes
))
1304 message
= self
.tray_icon
.format_episode_list(new_episodes
)
1306 #auto download new episodes
1307 if gl
.config
.auto_download_when_minimized
:
1308 message
+= '\n<i>(%s...)</i>' % _('downloading')
1309 self
.download_episode_list(new_episodes
)
1310 self
.tray_icon
.send_notification(message
, title
)
1313 # open the episodes selection dialog
1315 self
.on_itemDownloadAllNew_activate( self
.gPodder
)
1317 def update_feed_cache_proc( self
, force_update
, callback_proc
= None, callback_error
= None, finish_proc
= None):
1318 if not force_update
:
1319 self
.channels
= load_channels()
1321 is_cancelled_cb
= lambda: self
.feed_cache_update_cancelled
1322 self
.channels
= update_channels(callback_proc
=callback_proc
, callback_error
=callback_error
, is_cancelled_cb
=is_cancelled_cb
)
1324 self
.pbFeedUpdate
.set_text(_('Building list...'))
1328 def on_btnCancelFeedUpdate_clicked(self
, widget
):
1329 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
1330 self
.feed_cache_update_cancelled
= True
1332 def update_feed_cache(self
, force_update
=True, notify_no_new_episodes
=False, select_url_afterwards
=None):
1334 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
1336 # let's get down to business..
1337 callback_proc
= lambda pos
, count
: util
.idle_add(self
.update_feed_cache_callback
, self
.pbFeedUpdate
, pos
, count
, force_update
)
1338 finish_proc
= lambda: util
.idle_add(self
.update_feed_cache_finish_callback
, force_update
, notify_no_new_episodes
, select_url_afterwards
)
1340 self
.feed_cache_update_cancelled
= False
1341 self
.btnUpdateFeeds
.hide_all()
1342 self
.hboxUpdateFeeds
.show_all()
1344 args
= (force_update
, callback_proc
, self
.notification
, finish_proc
)
1346 thread
= Thread( target
= self
.update_feed_cache_proc
, args
= args
)
1349 def download_podcast_by_url( self
, url
, want_message_dialog
= True, widget
= None):
1350 if self
.active_channel
is None:
1353 current_channel
= self
.active_channel
1354 current_podcast
= current_channel
.find_episode( url
)
1355 filename
= current_podcast
.local_filename()
1358 if (widget
.get_name() == 'itemPlaySelected' or widget
.get_name() == 'toolPlay') and os
.path
.exists( filename
):
1359 # addDownloadedItem just to make sure the episode is marked correctly in localdb
1360 current_channel
.addDownloadedItem( current_podcast
)
1362 if current_podcast
.file_type() != 'torrent':
1363 self
.playback_episode( current_channel
, current_podcast
)
1366 if widget
.get_name() == 'treeAvailable' or widget
.get_name() == 'item_episode_details':
1367 play_callback
= lambda: self
.playback_episode( current_channel
, current_podcast
)
1368 download_callback
= lambda: self
.download_podcast_by_url( url
, want_message_dialog
, None)
1369 gpe
= gPodderEpisode( episode
= current_podcast
, channel
= current_channel
, download_callback
= download_callback
, play_callback
= play_callback
)
1372 if not os
.path
.exists( filename
) and not services
.download_status_manager
.is_download_in_progress( current_podcast
.url
):
1373 download
.DownloadThread( current_channel
, current_podcast
, self
.notification
).start()
1375 if want_message_dialog
and os
.path
.exists( filename
) and not current_podcast
.file_type() == 'torrent':
1376 title
= _('Episode already downloaded')
1377 message
= _('You have already downloaded this episode. Click on the episode to play it.')
1378 self
.show_message( message
, title
)
1379 elif want_message_dialog
and not current_podcast
.file_type() == 'torrent':
1380 title
= _('Download in progress')
1381 message
= _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
1382 self
.show_message( message
, title
)
1384 if os
.path
.exists( filename
):
1385 log( 'Episode has already been downloaded.')
1386 current_channel
.addDownloadedItem( current_podcast
)
1387 self
.updateComboBox()
1389 def on_gPodder_delete_event(self
, widget
, *args
):
1390 """Called when the GUI wants to close the window
1391 Displays a confirmation dialog (and closes/hides gPodder)
1394 downloading
= services
.download_status_manager
.has_items()
1396 # Only iconify if we are using the window's "X" button,
1397 # but not when we are using "Quit" in the menu or toolbar
1398 if not gl
.config
.on_quit_ask
and gl
.config
.on_quit_systray
and self
.tray_icon
and widget
.name
not in ('toolQuit', 'itemQuit'):
1399 self
.iconify_main_window()
1400 elif gl
.config
.on_quit_ask
or downloading
:
1401 if gpodder
.interface
== gpodder
.MAEMO
:
1402 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
1404 self
.close_gpodder()
1407 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
1408 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1409 dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
1411 title
= _('Quit gPodder')
1413 message
= _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1415 message
= _('Do you really want to quit gPodder now?')
1417 dialog
.set_title(title
)
1418 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
1420 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
1421 dialog
.vbox
.pack_start(cb_ask
)
1424 result
= dialog
.run()
1427 if result
== gtk
.RESPONSE_CLOSE
:
1428 if not downloading
and cb_ask
.get_active() == True:
1429 gl
.config
.on_quit_ask
= False
1430 self
.close_gpodder()
1432 self
.close_gpodder()
1436 def close_gpodder(self
):
1437 """ clean everything and exit properly
1440 if not save_channels(self
.channels
):
1441 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1443 services
.download_status_manager
.cancel_all()
1445 self
.gtk_main_quit()
1448 def get_old_episodes(self
):
1450 for channel
in self
.channels
:
1451 for episode
in channel
.get_downloaded_episodes():
1452 if episode
.is_old() and not episode
.is_locked
and episode
.is_played
:
1453 episodes
.append(episode
)
1456 def for_each_selected_episode_url( self
, callback
):
1457 ( model
, paths
) = self
.treeAvailable
.get_selection().get_selected_rows()
1459 url
= model
.get_value( model
.get_iter( path
), 0)
1462 except Exception, e
:
1463 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url
, e
, sender
= self
)
1464 self
.active_channel
.update_model()
1465 self
.updateComboBox()
1467 def delete_episode_list( self
, episodes
, confirm
= True):
1468 if len(episodes
) == 0:
1471 if len(episodes
) == 1:
1472 message
= _('Do you really want to delete this episode?')
1474 message
= _('Do you really want to delete %d episodes?') % len(episodes
)
1476 if confirm
and self
.show_confirmation( message
, _('Delete episodes')) == False:
1479 for episode
in episodes
:
1480 log('Deleting episode: %s', episode
.title
, sender
= self
)
1481 episode
.delete_from_disk()
1483 self
.download_status_updated()
1485 def on_itemRemoveOldEpisodes_activate( self
, widget
):
1487 ('title', _('Episode')),
1488 ('channel_prop', _('Podcast')),
1489 ('filesize_prop', _('Size')),
1490 ('pubdate_prop', _('Released')),
1491 ('played_prop', _('Status')),
1492 ('age_prop', _('Downloaded')),
1495 selection_buttons
= {
1496 _('Select played'): lambda episode
: episode
.is_played
,
1497 _('Select older than %d days') % gl
.config
.episode_old_age
: lambda episode
: episode
.is_old(),
1500 instructions
= _('Select the episodes you want to delete from your hard disk.')
1504 for channel
in self
.channels
:
1505 for episode
in channel
.get_downloaded_episodes():
1506 if not episode
.is_locked
:
1507 episodes
.append(episode
)
1508 selected
.append(episode
.is_played
)
1510 gPodderEpisodeSelector( title
= _('Remove old episodes'), instructions
= instructions
, \
1511 episodes
= episodes
, selected
= selected
, columns
= columns
, \
1512 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
1513 selection_buttons
= selection_buttons
)
1515 def mark_selected_episodes_new(self
):
1516 callback
= lambda url
: self
.active_channel
.find_episode(url
).mark_new()
1517 self
.for_each_selected_episode_url(callback
)
1519 def mark_selected_episodes_old(self
):
1520 callback
= lambda url
: self
.active_channel
.find_episode(url
).mark_old()
1521 self
.for_each_selected_episode_url(callback
)
1523 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
1525 callback
= lambda url
: db
.mark_episode(url
, is_played
=True, toggle
=True)
1527 callback
= lambda url
: db
.mark_episode(url
, is_played
=new_value
)
1529 self
.for_each_selected_episode_url(callback
)
1531 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
1533 callback
= lambda url
: db
.mark_episode(url
, is_locked
=True, toggle
=True)
1535 callback
= lambda url
: db
.mark_episode(url
, is_locked
=new_value
)
1537 self
.for_each_selected_episode_url(callback
)
1539 def on_item_email_subscriptions_activate(self
, widget
):
1540 if not self
.channels
:
1541 self
.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1542 elif not gl
.send_subscriptions():
1543 self
.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1545 def on_item_show_url_entry_activate(self
, widget
):
1546 gl
.config
.show_podcast_url_entry
= self
.item_show_url_entry
.get_active()
1548 def on_itemUpdate_activate(self
, widget
, notify_no_new_episodes
=False):
1549 restore_from
= can_restore_from_opml()
1552 self
.update_feed_cache(notify_no_new_episodes
=notify_no_new_episodes
)
1553 elif restore_from
is not None:
1554 title
= _('Database upgrade required')
1555 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?')
1556 if self
.show_confirmation(message
, title
):
1557 add_callback
= lambda url
: self
.add_new_channel(url
, False, True)
1558 w
= gtk
.Dialog(_('Migrating to SQLite'), self
.gPodder
, 0, (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_ACCEPT
))
1559 w
.set_has_separator(False)
1560 w
.set_response_sensitive(gtk
.RESPONSE_ACCEPT
, False)
1561 w
.set_default_size(500, -1)
1562 pb
= gtk
.ProgressBar()
1565 l
.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1566 l
.set_alignment(0.0, 0.5)
1567 w
.vbox
.pack_start(l
)
1570 l
.set_alignment(0.0, 0.5)
1571 l
.set_text(_('Please wait while your settings are converted.'))
1572 w
.vbox
.pack_start(l
)
1573 w
.vbox
.pack_start(pb
)
1575 lb
.set_ellipsize(pango
.ELLIPSIZE_END
)
1576 lb
.set_alignment(0.0, 0.5)
1577 lb
.set_padding(6, 6)
1578 w
.vbox
.pack_start(lb
)
1580 def set_pb_status(pb
, lb
, fraction
, text
):
1581 pb
.set_fraction(float(fraction
)/100.0)
1582 pb
.set_text('%.0f %%' % fraction
)
1583 lb
.set_markup('<i>%s</i>' % saxutils
.escape(text
))
1584 while gtk
.events_pending():
1585 gtk
.main_iteration(False)
1586 status_callback
= lambda fraction
, text
: set_pb_status(pb
, lb
, fraction
, text
)
1587 get_localdb
= lambda channel
: LocalDBReader(channel
.url
).read(channel
.index_file
)
1589 start
= datetime
.datetime
.now()
1590 gl
.migrate_to_sqlite(add_callback
, status_callback
, load_channels
, get_localdb
)
1591 # Refresh the view with the updated episodes
1592 for channel
in self
.channels
:
1593 channel
.force_update_tree_model()
1594 self
.updateComboBox()
1595 time_taken
= str(datetime
.datetime
.now()-start
)
1596 status_callback(100.0, _('Migration finished in %s') % time_taken
)
1597 w
.set_response_sensitive(gtk
.RESPONSE_ACCEPT
, True)
1601 title
= _('Import podcasts from the web')
1602 message
= _('Your podcast list is empty. Do you want to see a list of example podcasts you can subscribe to?')
1603 if self
.show_confirmation(message
, title
):
1604 self
.on_itemImportChannels_activate(self
, widget
)
1606 def download_episode_list( self
, episodes
):
1607 services
.download_status_manager
.start_batch_mode()
1608 for episode
in episodes
:
1609 log('Downloading episode: %s', episode
.title
, sender
= self
)
1610 filename
= episode
.local_filename()
1611 if not os
.path
.exists( filename
) and not services
.download_status_manager
.is_download_in_progress( episode
.url
):
1612 download
.DownloadThread( episode
.channel
, episode
, self
.notification
).start()
1613 services
.download_status_manager
.end_batch_mode()
1615 def new_episodes_show(self
, episodes
):
1617 ('title', _('Episode')),
1618 ('channel_prop', _('Podcast')),
1619 ('filesize_prop', _('Size')),
1620 ('pubdate_prop', _('Released')),
1623 if len(episodes
) > 0:
1624 instructions
= _('Select the episodes you want to download now.')
1626 gPodderEpisodeSelector(title
=_('New episodes available'), instructions
=instructions
, \
1627 episodes
=episodes
, columns
=columns
, selected_default
=True, \
1628 stock_ok_button
= 'gpodder-download', \
1629 callback
=self
.download_episode_list
)
1631 title
= _('No new episodes')
1632 message
= _('No new episodes to download.\nPlease check for new episodes later.')
1633 self
.show_message(message
, title
)
1635 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
1637 for channel
in self
.channels
:
1638 for episode
in channel
.get_new_episodes():
1639 episodes
.append(episode
)
1640 self
.new_episodes_show(episodes
)
1642 def get_all_episodes(self
, exclude_nonsignificant
=True ):
1643 """'exclude_nonsignificant' will exclude non-downloaded episodes
1644 and all episodes from channels that are set to skip when syncing"""
1646 for channel
in self
.channels
:
1647 if not channel
.sync_to_devices
and exclude_nonsignificant
:
1648 log('Skipping channel: %s', channel
.title
, sender
=self
)
1650 for episode
in channel
.get_all_episodes():
1651 if episode
.was_downloaded(and_exists
=True) or not exclude_nonsignificant
:
1652 episode_list
.append(episode
)
1655 def ipod_delete_played(self
, device
):
1656 all_episodes
= self
.get_all_episodes( exclude_nonsignificant
=False )
1657 episodes_on_device
= device
.get_all_tracks()
1658 for local_episode
in all_episodes
:
1659 if local_episode
.is_played
and not local_episode
.is_locked
or local_episode
.is_deleted
:
1660 if gl
.config
.device_type
== 'filesystem':
1661 local_episode_name
= util
.sanitize_filename(local_episode
.sync_filename(), gl
.config
.mp3_player_max_filename_length
)
1663 local_episode_name
= local_episode
.sync_filename()
1664 for device_episode
in episodes_on_device
:
1665 if device_episode
.title
== local_episode_name
:
1666 log("mp3_player_delete_played: removing %s" % device_episode
.title
)
1667 device
.remove_track(device_episode
)
1670 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
1671 Thread(target
=self
.sync_to_ipod_thread
, args
=(widget
, episodes
)).start()
1673 def sync_to_ipod_thread(self
, widget
, episodes
=None):
1674 device
= sync
.open_device()
1677 title
= _('No device configured')
1678 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1679 self
.notification(message
, title
)
1682 if not device
.open():
1683 title
= _('Cannot open device')
1684 message
= _('There has been an error opening your device.')
1685 self
.notification(message
, title
)
1688 gPodderSync(device
=device
, gPodder
=self
)
1690 self
.tray_icon
.set_synchronisation_device(device
)
1692 if episodes
is None:
1693 episodes_to_sync
= self
.get_all_episodes()
1694 device
.add_tracks(episodes_to_sync
)
1695 # 'only_sync_not_played' must be used or else all the played
1696 # tracks will be copied then immediately deleted
1697 if gl
.config
.mp3_player_delete_played
and gl
.config
.only_sync_not_played
:
1698 self
.ipod_delete_played(device
)
1700 device
.add_tracks(episodes
, force_played
=True)
1702 if not device
.close():
1703 title
= _('Error closing device')
1704 message
= _('There has been an error closing your device.')
1705 self
.notification(message
, title
)
1709 self
.tray_icon
.release_synchronisation_device()
1711 # update model for played state updates after sync
1712 for channel
in self
.channels
:
1713 util
.idle_add(channel
.update_model
)
1714 util
.idle_add(self
.updateComboBox
)
1716 def ipod_cleanup_callback(self
, device
, tracks
):
1717 title
= _('Delete podcasts from device?')
1718 message
= _('Do you really want to completely remove the selected episodes?')
1719 if len(tracks
) > 0 and self
.show_confirmation(message
, title
):
1720 device
.remove_tracks(tracks
)
1722 if not device
.close():
1723 title
= _('Error closing device')
1724 message
= _('There has been an error closing your device.')
1725 self
.show_message(message
, title
)
1728 def on_cleanup_ipod_activate(self
, widget
, *args
):
1730 ('title', _('Episode')),
1731 ('podcast', _('Podcast')),
1732 ('filesize', _('Size')),
1733 ('modified', _('Copied')),
1734 ('playcount', _('Play count')),
1735 ('released', _('Released')),
1738 device
= sync
.open_device()
1741 title
= _('No device configured')
1742 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1743 self
.show_message(message
, title
)
1746 if not device
.open():
1747 title
= _('Cannot open device')
1748 message
= _('There has been an error opening your device.')
1749 self
.show_message(message
, title
)
1752 gPodderSync(device
=device
, gPodder
=self
)
1754 tracks
= device
.get_all_tracks()
1756 remove_tracks_callback
= lambda tracks
: self
.ipod_cleanup_callback(device
, tracks
)
1758 for key
, caption
in columns
:
1759 want_this_column
= False
1760 for track
in tracks
:
1761 if getattr(track
, key
) is not None:
1762 want_this_column
= True
1765 if want_this_column
:
1766 wanted_columns
.append((key
, caption
))
1767 title
= _('Remove podcasts from device')
1768 instructions
= _('Select the podcast episodes you want to remove from your device.')
1769 gPodderEpisodeSelector(title
=title
, instructions
=instructions
, episodes
=tracks
, columns
=wanted_columns
, \
1770 stock_ok_button
=gtk
.STOCK_DELETE
, callback
=remove_tracks_callback
)
1772 title
= _('No files on device')
1773 message
= _('The devices contains no files to be removed.')
1774 self
.show_message(message
, title
)
1776 def show_hide_tray_icon(self
):
1777 if gl
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
1778 self
.tray_icon
= trayicon
.GPodderStatusIcon(self
, scalable_dir
)
1779 elif not gl
.config
.display_tray_icon
and self
.tray_icon
is not None:
1780 self
.tray_icon
.set_visible(False)
1782 self
.tray_icon
= None
1784 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
1785 self
.tray_icon
.set_visible(self
.minimized
)
1786 elif self
.tray_icon
:
1787 self
.tray_icon
.set_visible(True)
1789 def on_itemShowToolbar_activate(self
, widget
):
1790 gl
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
1792 def on_itemShowDescription_activate(self
, widget
):
1793 gl
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
1795 def update_item_device( self
):
1796 if gl
.config
.device_type
!= 'none':
1797 self
.itemDevice
.show_all()
1798 (label
,) = self
.itemDevice
.get_children()
1799 label
.set_text(gl
.get_device_name())
1801 self
.itemDevice
.hide_all()
1803 def properties_closed( self
):
1804 self
.show_hide_tray_icon()
1805 self
.update_item_device()
1806 self
.updateComboBox()
1808 def on_itemPreferences_activate(self
, widget
, *args
):
1809 if gpodder
.interface
== gpodder
.GUI
:
1810 gPodderProperties(callback_finished
=self
.properties_closed
, user_apps_reader
=self
.user_apps_reader
)
1812 gPodderMaemoPreferences()
1814 def on_itemAddChannel_activate(self
, widget
, *args
):
1815 if gpodder
.interface
== gpodder
.MAEMO
or not gl
.config
.show_podcast_url_entry
:
1816 gPodderAddPodcastDialog(url_callback
=self
.add_new_channel
)
1818 if self
.channelPaned
.get_position() < 200:
1819 self
.channelPaned
.set_position( 200)
1820 self
.entryAddChannel
.grab_focus()
1822 def on_itemEditChannel_activate(self
, widget
, *args
):
1823 if self
.active_channel
is None:
1824 title
= _('No podcast selected')
1825 message
= _('Please select a podcast in the podcasts list to edit.')
1826 self
.show_message( message
, title
)
1829 gPodderChannel(channel
=self
.active_channel
, callback_closed
=self
.updateComboBox
, callback_change_url
=self
.change_channel_url
)
1831 def change_channel_url(self
, old_url
, new_url
):
1834 channel
= podcastChannel
.load(url
=new_url
, create
=True)
1839 self
.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1842 for channel
in self
.channels
:
1843 if channel
.url
== old_url
:
1844 log('=> change channel url from %s to %s', old_url
, new_url
)
1845 old_save_dir
= channel
.save_dir
1846 channel
.url
= new_url
1847 new_save_dir
= channel
.save_dir
1848 log('old save dir=%s', old_save_dir
, sender
=self
)
1849 log('new save dir=%s', new_save_dir
, sender
=self
)
1850 files
= glob
.glob(os
.path
.join(old_save_dir
, '*'))
1851 log('moving %d files to %s', len(files
), new_save_dir
, sender
=self
)
1853 log('moving %s', file, sender
=self
)
1854 shutil
.move(file, new_save_dir
)
1856 os
.rmdir(old_save_dir
)
1858 log('Warning: cannot delete %s', old_save_dir
, sender
=self
)
1860 save_channels(self
.channels
)
1861 # update feed cache and select the podcast with the new URL afterwards
1862 self
.update_feed_cache(force_update
=False, select_url_afterwards
=new_url
)
1864 def on_itemRemoveChannel_activate(self
, widget
, *args
):
1866 if gpodder
.interface
== gpodder
.GUI
:
1867 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
1868 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
1869 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
1871 title
= _('Remove podcast and episodes?')
1872 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
1874 dialog
.set_title(title
)
1875 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
1877 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
1878 dialog
.vbox
.pack_start(cb_ask
)
1880 affirmative
= gtk
.RESPONSE_YES
1881 elif gpodder
.interface
== gpodder
.MAEMO
:
1882 cb_ask
= gtk
.CheckButton('') # dummy check button
1883 dialog
= hildon
.Note('confirmation', (self
.gPodder
, _('Do you really want to remove this podcast and all downloaded episodes?')))
1884 affirmative
= gtk
.RESPONSE_OK
1886 result
= dialog
.run()
1889 if result
== affirmative
:
1890 # delete downloaded episodes only if checkbox is unchecked
1891 if cb_ask
.get_active() == False:
1892 self
.active_channel
.remove_downloaded()
1894 log('Not removing downloaded episodes', sender
=self
)
1896 # only delete partial files if we do not have any downloads in progress
1897 delete_partial
= not services
.download_status_manager
.has_items()
1898 gl
.clean_up_downloads(delete_partial
)
1900 # get the URL of the podcast we want to select next
1901 position
= self
.channels
.index(self
.active_channel
)
1902 if position
== len(self
.channels
)-1:
1903 # this is the last podcast, so select the URL
1904 # of the item before this one (i.e. the "new last")
1905 select_url
= self
.channels
[position
-1].url
1907 # there is a podcast after the deleted one, so
1908 # we simply select the one that comes after it
1909 select_url
= self
.channels
[position
+1].url
1911 # Remove the channel
1912 self
.active_channel
.delete()
1913 self
.channels
.remove(self
.active_channel
)
1914 save_channels(self
.channels
)
1916 # Re-load the channels and select the desired new channel
1917 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
1919 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
1921 def get_opml_filter(self
):
1922 filter = gtk
.FileFilter()
1923 filter.add_pattern('*.opml')
1924 filter.add_pattern('*.xml')
1925 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
1928 def on_item_import_from_file_activate(self
, widget
, filename
=None):
1929 if filename
is None:
1930 if gpodder
.interface
== gpodder
.GUI
:
1931 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
1932 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1933 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
1934 elif gpodder
.interface
== gpodder
.MAEMO
:
1935 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
1936 dlg
.set_filter(self
.get_opml_filter())
1937 response
= dlg
.run()
1939 if response
== gtk
.RESPONSE_OK
:
1940 filename
= dlg
.get_filename()
1943 if filename
is not None:
1944 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), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
1946 def on_itemExportChannels_activate(self
, widget
, *args
):
1947 if not self
.channels
:
1948 title
= _('Nothing to export')
1949 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1950 self
.show_message( message
, title
)
1953 if gpodder
.interface
== gpodder
.GUI
:
1954 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
1955 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1956 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
1957 elif gpodder
.interface
== gpodder
.MAEMO
:
1958 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
1959 dlg
.set_filter(self
.get_opml_filter())
1960 response
= dlg
.run()
1961 if response
== gtk
.RESPONSE_OK
:
1962 filename
= dlg
.get_filename()
1963 exporter
= opml
.Exporter( filename
)
1964 if not exporter
.write( self
.channels
):
1965 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1969 def on_itemImportChannels_activate(self
, widget
, *args
):
1970 gPodderOpmlLister().get_channels_from_url(gl
.config
.opml_url
, lambda url
: self
.add_new_channel(url
,False), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
1972 def on_btnTransfer_clicked(self
, widget
, *args
):
1973 self
.on_treeAvailable_row_activated( widget
, args
)
1975 def on_homepage_activate(self
, widget
, *args
):
1976 util
.open_website(app_website
)
1978 def on_wiki_activate(self
, widget
, *args
):
1979 util
.open_website('http://wiki.gpodder.org/')
1981 def on_bug_tracker_activate(self
, widget
, *args
):
1982 util
.open_website('http://bugs.gpodder.org/')
1984 def on_itemAbout_activate(self
, widget
, *args
):
1985 dlg
= gtk
.AboutDialog()
1986 dlg
.set_name(app_name
.replace('p', 'P')) # gpodder->gPodder
1987 dlg
.set_version( app_version
)
1988 dlg
.set_copyright( app_copyright
)
1989 dlg
.set_website( app_website
)
1990 dlg
.set_translator_credits( _('translator-credits'))
1991 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
1993 if gpodder
.interface
== gpodder
.GUI
:
1994 # For the "GUI" version, we add some more
1995 # items to the about dialog (credits and logo)
1996 dlg
.set_authors(app_authors
)
1998 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file_at_size(scalable_dir
, 200, 200))
2004 def on_wNotebook_switch_page(self
, widget
, *args
):
2006 if gpodder
.interface
== gpodder
.MAEMO
:
2007 page
= self
.wNotebook
.get_nth_page(page_num
)
2008 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2009 if page_num
== 0 and self
.active_channel
is not None:
2010 self
.set_title(self
.active_channel
.title
)
2012 self
.set_title(tab_label
)
2014 self
.play_or_download()
2016 self
.toolDownload
.set_sensitive( False)
2017 self
.toolPlay
.set_sensitive( False)
2018 self
.toolTransfer
.set_sensitive( False)
2019 self
.toolCancel
.set_sensitive( services
.download_status_manager
.has_items())
2021 def on_treeChannels_row_activated(self
, widget
, *args
):
2022 self
.on_itemEditChannel_activate( self
.treeChannels
)
2024 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2025 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2027 if model
is not None and iter != None:
2028 id = model
.get_path( iter)[0]
2029 self
.active_channel
= self
.channels
[id]
2031 if gpodder
.interface
== gpodder
.MAEMO
:
2032 self
.set_title(self
.active_channel
.title
)
2033 self
.itemEditChannel
.show_all()
2034 self
.itemRemoveChannel
.show_all()
2036 self
.active_channel
= None
2037 self
.itemEditChannel
.hide_all()
2038 self
.itemRemoveChannel
.hide_all()
2040 self
.updateTreeView()
2042 def on_entryAddChannel_changed(self
, widget
, *args
):
2043 active
= self
.entryAddChannel
.get_text() not in ('', self
.ENTER_URL_TEXT
)
2044 self
.btnAddChannel
.set_sensitive( active
)
2046 def on_btnAddChannel_clicked(self
, widget
, *args
):
2047 url
= self
.entryAddChannel
.get_text()
2048 self
.entryAddChannel
.set_text('')
2049 self
.add_new_channel( url
)
2051 def on_btnEditChannel_clicked(self
, widget
, *args
):
2052 self
.on_itemEditChannel_activate( widget
, args
)
2054 def on_treeAvailable_row_activated(self
, widget
, *args
):
2056 selection
= self
.treeAvailable
.get_selection()
2057 selection_tuple
= selection
.get_selected_rows()
2058 transfer_files
= False
2061 if selection
.count_selected_rows() > 1:
2062 widget_to_send
= None
2063 show_message_dialog
= False
2065 widget_to_send
= widget
2066 show_message_dialog
= True
2068 if widget
.get_name() == 'itemTransferSelected' or widget
.get_name() == 'toolTransfer':
2069 transfer_files
= True
2071 services
.download_status_manager
.start_batch_mode()
2072 for apath
in selection_tuple
[1]:
2073 selection_iter
= self
.treeAvailable
.get_model().get_iter( apath
)
2074 url
= self
.treeAvailable
.get_model().get_value( selection_iter
, 0)
2077 episodes
.append( self
.active_channel
.find_episode( url
))
2079 self
.download_podcast_by_url( url
, show_message_dialog
, widget_to_send
)
2080 services
.download_status_manager
.end_batch_mode()
2082 if transfer_files
and len(episodes
):
2083 self
.on_sync_to_ipod_activate(None, episodes
)
2085 title
= _('Nothing selected')
2086 message
= _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
2087 self
.show_message( message
, title
)
2089 def on_btnDownload_clicked(self
, widget
, *args
):
2090 self
.on_treeAvailable_row_activated( widget
, args
)
2092 def on_treeAvailable_button_release_event(self
, widget
, *args
):
2093 self
.play_or_download()
2095 def auto_update_procedure(self
, first_run
=False):
2096 log('auto_update_procedure() got called', sender
=self
)
2097 if not first_run
and gl
.config
.auto_update_feeds
and self
.minimized
:
2098 self
.update_feed_cache(force_update
=True)
2100 next_update
= 60*1000*gl
.config
.auto_update_frequency
2101 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2103 def on_treeDownloads_row_activated(self
, widget
, *args
):
2106 if self
.wNotebook
.get_current_page() > 0:
2107 # Use the download list treeview + model
2108 ( tree
, column
) = ( self
.treeDownloads
, 3 )
2110 # Use the available podcasts treeview + model
2111 ( tree
, column
) = ( self
.treeAvailable
, 0 )
2113 selection
= tree
.get_selection()
2114 (model
, paths
) = selection
.get_selected_rows()
2116 url
= model
.get_value( model
.get_iter( path
), column
)
2117 cancel_urls
.append( url
)
2119 if len( cancel_urls
) == 0:
2120 log('Nothing selected.', sender
= self
)
2123 if len( cancel_urls
) == 1:
2124 title
= _('Cancel download?')
2125 message
= _("Cancelling this download will remove the partially downloaded file and stop the download.")
2127 title
= _('Cancel downloads?')
2128 message
= _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection
.count_selected_rows()
2130 if self
.show_confirmation( message
, title
):
2131 services
.download_status_manager
.start_batch_mode()
2132 for url
in cancel_urls
:
2133 services
.download_status_manager
.cancel_by_url( url
)
2134 services
.download_status_manager
.end_batch_mode()
2136 def on_btnCancelDownloadStatus_clicked(self
, widget
, *args
):
2137 self
.on_treeDownloads_row_activated( widget
, None)
2139 def on_btnCancelAll_clicked(self
, widget
, *args
):
2140 self
.treeDownloads
.get_selection().select_all()
2141 self
.on_treeDownloads_row_activated( self
.toolCancel
, None)
2142 self
.treeDownloads
.get_selection().unselect_all()
2144 def on_btnDownloadedExecute_clicked(self
, widget
, *args
):
2145 self
.on_treeAvailable_row_activated( widget
, args
)
2147 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2148 if self
.active_channel
is None:
2151 channel_url
= self
.active_channel
.url
2152 selection
= self
.treeAvailable
.get_selection()
2153 ( model
, paths
) = selection
.get_selected_rows()
2155 if selection
.count_selected_rows() == 0:
2156 log( 'Nothing selected - will not remove any downloaded episode.')
2159 if selection
.count_selected_rows() == 1:
2160 episode_title
= saxutils
.escape(model
.get_value(model
.get_iter(paths
[0]), 1))
2162 episode
= db
.load_episode(model
.get_value(model
.get_iter(paths
[0]), 0))
2163 if episode
['is_locked']:
2164 title
= _('%s is locked') % episode_title
2165 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2166 self
.notification(message
, title
)
2169 title
= _('Remove %s?') % episode_title
2170 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.")
2172 title
= _('Remove %d episodes?') % selection
.count_selected_rows()
2173 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.')
2177 episode
= db
.load_episode(model
.get_value(model
.get_iter(path
), 0))
2178 if episode
['is_locked']:
2181 if selection
.count_selected_rows() == locked_count
:
2182 title
= _('Episodes are locked')
2183 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2184 self
.notification(message
, title
)
2186 elif locked_count
> 0:
2187 title
= _('Remove %d out of %d episodes?') % (selection
.count_selected_rows() - locked_count
, selection
.count_selected_rows())
2188 message
= _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
2190 # if user confirms deletion, let's remove some stuff ;)
2191 if self
.show_confirmation( message
, title
):
2193 # iterate over the selection, see also on_treeDownloads_row_activated
2195 url
= model
.get_value( model
.get_iter( path
), 0)
2196 self
.active_channel
.delete_episode_by_url( url
)
2198 # now, clear local db cache so we can re-read it
2199 self
.updateComboBox()
2201 log( 'Error while deleting (some) downloads.')
2203 # only delete partial files if we do not have any downloads in progress
2204 delete_partial
= not services
.download_status_manager
.has_items()
2205 gl
.clean_up_downloads(delete_partial
)
2206 self
.active_channel
.force_update_tree_model()
2207 self
.updateTreeView()
2209 def on_key_press(self
, widget
, event
):
2210 # Currently, we only handle Maemo hardware keys here,
2211 # so if we are not a Maemo app, we don't do anything!
2212 if gpodder
.interface
!= gpodder
.MAEMO
:
2215 if event
.keyval
== gtk
.keysyms
.F6
:
2217 self
.window
.unfullscreen()
2219 self
.window
.fullscreen()
2220 if event
.keyval
== gtk
.keysyms
.Escape
:
2221 new_visibility
= not self
.vboxChannelNavigator
.get_property('visible')
2222 self
.vboxChannelNavigator
.set_property('visible', new_visibility
)
2223 self
.column_size
.set_visible(not new_visibility
)
2224 self
.column_released
.set_visible(not new_visibility
)
2227 if event
.keyval
== gtk
.keysyms
.F7
: #plus
2229 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
2233 selection
= self
.treeChannels
.get_selection()
2234 (model
, iter) = selection
.get_selected()
2235 selection
.select_path(((model
.get_path(iter)[0]+diff
)%len(model
),))
2236 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2238 def window_state_event(self
, widget
, event
):
2239 if event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_FULLSCREEN
:
2240 self
.fullscreen
= True
2242 self
.fullscreen
= False
2244 old_minimized
= self
.minimized
2246 if event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_ICONIFIED
:
2247 self
.minimized
= True
2249 self
.minimized
= False
2251 if old_minimized
!= self
.minimized
and self
.tray_icon
:
2252 self
.gPodder
.set_skip_taskbar_hint(self
.minimized
)
2253 elif not self
.tray_icon
:
2254 self
.gPodder
.set_skip_taskbar_hint(False)
2256 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
2257 self
.tray_icon
.set_visible(self
.minimized
)
2259 def uniconify_main_window(self
):
2261 self
.gPodder
.present()
2263 def iconify_main_window(self
):
2264 if not self
.minimized
:
2265 self
.gPodder
.iconify()
2267 class gPodderChannel(GladeWidget
):
2268 finger_friendly_widgets
= ['btn_website', 'btnOK', 'channel_description']
2271 global WEB_BROWSER_ICON
2272 self
.changed
= False
2273 self
.image3167
.set_property('icon-name', WEB_BROWSER_ICON
)
2274 self
.gPodderChannel
.set_title( self
.channel
.title
)
2275 self
.entryTitle
.set_text( self
.channel
.title
)
2276 self
.entryURL
.set_text( self
.channel
.url
)
2278 self
.LabelDownloadTo
.set_text( self
.channel
.save_dir
)
2279 self
.LabelWebsite
.set_text( self
.channel
.link
)
2281 self
.cbNoSync
.set_active( not self
.channel
.sync_to_devices
)
2282 self
.musicPlaylist
.set_text(self
.channel
.device_playlist_name
)
2283 if self
.channel
.username
:
2284 self
.FeedUsername
.set_text( self
.channel
.username
)
2285 if self
.channel
.password
:
2286 self
.FeedPassword
.set_text( self
.channel
.password
)
2288 services
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
2289 services
.cover_downloader
.request_cover(self
.channel
)
2291 # Hide the website button if we don't have a valid URL
2292 if not self
.channel
.link
:
2293 self
.btn_website
.hide_all()
2295 b
= gtk
.TextBuffer()
2296 b
.set_text( self
.channel
.description
)
2297 self
.channel_description
.set_buffer( b
)
2299 #Add Drag and Drop Support
2300 flags
= gtk
.DEST_DEFAULT_ALL
2301 targets
= [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2302 actions
= gtk
.gdk
.ACTION_DEFAULT | gtk
.gdk
.ACTION_COPY
2303 self
.vboxCoverEditor
.drag_dest_set( flags
, targets
, actions
)
2304 self
.vboxCoverEditor
.connect( 'drag_data_received', self
.drag_data_received
)
2306 def on_btn_website_clicked(self
, widget
):
2307 util
.open_website(self
.channel
.link
)
2309 def on_btnDownloadCover_clicked(self
, widget
):
2310 if gpodder
.interface
== gpodder
.GUI
:
2311 dlg
= gtk
.FileChooserDialog(title
=_('Select new podcast cover artwork'), parent
=self
.gPodderChannel
, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2312 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2313 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2314 elif gpodder
.interface
== gpodder
.MAEMO
:
2315 dlg
= hildon
.FileChooserDialog(self
.gPodderChannel
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2317 if dlg
.run() == gtk
.RESPONSE_OK
:
2319 services
.cover_downloader
.replace_cover(self
.channel
, url
)
2323 def on_btnClearCover_clicked(self
, widget
):
2324 services
.cover_downloader
.replace_cover(self
.channel
)
2326 def cover_download_finished(self
, channel_url
, pixbuf
):
2327 if pixbuf
is not None:
2328 self
.imgCover
.set_from_pixbuf(pixbuf
)
2329 self
.gPodderChannel
.show()
2331 def drag_data_received( self
, widget
, content
, x
, y
, sel
, ttype
, time
):
2332 files
= sel
.data
.strip().split('\n')
2334 self
.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2339 if file.startswith('file://') or file.startswith('http://'):
2340 services
.cover_downloader
.replace_cover(self
.channel
, file)
2343 self
.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2345 def on_gPodderChannel_destroy(self
, widget
, *args
):
2346 services
.cover_downloader
.unregister('cover-available', self
.cover_download_finished
)
2348 def on_btnOK_clicked(self
, widget
, *args
):
2349 entered_url
= self
.entryURL
.get_text()
2350 channel_url
= self
.channel
.url
2352 if entered_url
!= channel_url
:
2353 if self
.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils
.escape(entered_url
),), _('Really change URL?')):
2354 if hasattr(self
, 'callback_change_url'):
2355 self
.gPodderChannel
.hide_all()
2356 self
.callback_change_url(channel_url
, entered_url
)
2358 self
.channel
.sync_to_devices
= not self
.cbNoSync
.get_active()
2359 self
.channel
.device_playlist_name
= self
.musicPlaylist
.get_text()
2360 self
.channel
.set_custom_title( self
.entryTitle
.get_text())
2361 self
.channel
.username
= self
.FeedUsername
.get_text().strip()
2362 self
.channel
.password
= self
.FeedPassword
.get_text()
2365 self
.gPodderChannel
.destroy()
2366 self
.callback_closed()
2368 class gPodderAddPodcastDialog(GladeWidget
):
2369 finger_friendly_widgets
= ['btn_close', 'btn_add']
2372 if not hasattr(self
, 'url_callback'):
2373 log('No url callback set', sender
=self
)
2374 self
.url_callback
= None
2376 def on_btn_close_clicked(self
, widget
):
2377 self
.gPodderAddPodcastDialog
.destroy()
2379 def on_entry_url_changed(self
, widget
):
2380 self
.btn_add
.set_sensitive(self
.entry_url
.get_text().strip() != '')
2382 def on_btn_add_clicked(self
, widget
):
2383 url
= self
.entry_url
.get_text()
2384 self
.on_btn_close_clicked(widget
)
2385 if self
.url_callback
is not None:
2386 self
.url_callback(url
)
2389 class gPodderMaemoPreferences(GladeWidget
):
2390 finger_friendly_widgets
= ['btn_close', 'label128', 'label129', 'btn_advanced']
2393 gl
.config
.connect_gtk_togglebutton('update_on_startup', self
.update_on_startup
)
2394 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.show_tray_icon
)
2395 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.show_notifications
)
2396 gl
.config
.connect_gtk_togglebutton('on_quit_ask', self
.on_quit_ask
)
2398 self
.restart_required
= False
2399 self
.show_tray_icon
.connect('clicked', self
.on_restart_required
)
2400 self
.show_notifications
.connect('clicked', self
.on_restart_required
)
2402 def on_restart_required(self
, widget
):
2403 self
.restart_required
= True
2405 def on_btn_advanced_clicked(self
, widget
):
2406 self
.gPodderMaemoPreferences
.destroy()
2407 gPodderConfigEditor()
2409 def on_btn_close_clicked(self
, widget
):
2410 self
.gPodderMaemoPreferences
.destroy()
2411 if self
.restart_required
:
2412 self
.show_message(_('Please restart gPodder for the changes to take effect.'))
2415 class gPodderProperties(GladeWidget
):
2417 if not hasattr( self
, 'callback_finished'):
2418 self
.callback_finished
= None
2420 if gpodder
.interface
== gpodder
.MAEMO
:
2421 self
.table13
.hide_all() # bluetooth
2422 self
.table5
.hide_all() # player
2423 self
.table6
.hide_all() # bittorrent
2424 self
.gPodderProperties
.fullscreen()
2426 gl
.config
.connect_gtk_editable( 'http_proxy', self
.httpProxy
)
2427 gl
.config
.connect_gtk_editable( 'ftp_proxy', self
.ftpProxy
)
2428 gl
.config
.connect_gtk_editable( 'player', self
.openApp
)
2429 gl
.config
.connect_gtk_editable('videoplayer', self
.openVideoApp
)
2430 gl
.config
.connect_gtk_editable( 'custom_sync_name', self
.entryCustomSyncName
)
2431 gl
.config
.connect_gtk_togglebutton( 'custom_sync_name_enabled', self
.cbCustomSyncName
)
2432 gl
.config
.connect_gtk_togglebutton( 'auto_download_when_minimized', self
.downloadnew
)
2433 gl
.config
.connect_gtk_togglebutton( 'use_gnome_bittorrent', self
.radio_gnome_bittorrent
)
2434 gl
.config
.connect_gtk_togglebutton( 'update_on_startup', self
.updateonstartup
)
2435 gl
.config
.connect_gtk_togglebutton( 'only_sync_not_played', self
.only_sync_not_played
)
2436 gl
.config
.connect_gtk_togglebutton( 'fssync_channel_subfolders', self
.cbChannelSubfolder
)
2437 gl
.config
.connect_gtk_togglebutton( 'on_sync_mark_played', self
.on_sync_mark_played
)
2438 gl
.config
.connect_gtk_togglebutton( 'on_sync_delete', self
.on_sync_delete
)
2439 gl
.config
.connect_gtk_togglebutton( 'proxy_use_environment', self
.cbEnvironmentVariables
)
2440 gl
.config
.connect_gtk_filechooser( 'bittorrent_dir', self
.chooserBitTorrentTo
)
2441 gl
.config
.connect_gtk_spinbutton('episode_old_age', self
.episode_old_age
)
2442 gl
.config
.connect_gtk_togglebutton('auto_remove_old_episodes', self
.auto_remove_old_episodes
)
2443 gl
.config
.connect_gtk_togglebutton('auto_update_feeds', self
.auto_update_feeds
)
2444 gl
.config
.connect_gtk_spinbutton('auto_update_frequency', self
.auto_update_frequency
)
2445 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.display_tray_icon
)
2446 gl
.config
.connect_gtk_togglebutton('minimize_to_tray', self
.minimize_to_tray
)
2447 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.enable_notifications
)
2448 gl
.config
.connect_gtk_togglebutton('start_iconified', self
.start_iconified
)
2449 gl
.config
.connect_gtk_togglebutton('bluetooth_enabled', self
.bluetooth_enabled
)
2450 gl
.config
.connect_gtk_togglebutton('bluetooth_ask_always', self
.bluetooth_ask_always
)
2451 gl
.config
.connect_gtk_togglebutton('bluetooth_ask_never', self
.bluetooth_ask_never
)
2452 gl
.config
.connect_gtk_togglebutton('bluetooth_use_converter', self
.bluetooth_use_converter
)
2453 gl
.config
.connect_gtk_filechooser( 'bluetooth_converter', self
.bluetooth_converter
, is_for_files
=True)
2454 gl
.config
.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self
.ipod_write_gtkpod_extended
)
2455 gl
.config
.connect_gtk_togglebutton('mp3_player_delete_played', self
.delete_episodes_marked_played
)
2457 self
.enable_notifications
.set_sensitive(self
.display_tray_icon
.get_active())
2458 self
.minimize_to_tray
.set_sensitive(self
.display_tray_icon
.get_active())
2460 self
.entryCustomSyncName
.set_sensitive( self
.cbCustomSyncName
.get_active())
2462 self
.radio_gnome_bittorrent
.set_active(gl
.config
.use_gnome_bittorrent
)
2463 self
.radio_copy_torrents
.set_active(not gl
.config
.use_gnome_bittorrent
)
2465 self
.iPodMountpoint
.set_label( gl
.config
.ipod_mount
)
2466 self
.filesystemMountpoint
.set_label( gl
.config
.mp3_player_folder
)
2467 self
.bluetooth_device_name
.set_markup('<b>%s</b>'%gl
.config
.bluetooth_device_name
)
2468 self
.chooserDownloadTo
.set_current_folder(gl
.downloaddir
)
2470 self
.on_sync_delete
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2471 self
.on_sync_mark_played
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2473 if tagging_supported():
2474 gl
.config
.connect_gtk_togglebutton( 'update_tags', self
.updatetags
)
2476 self
.updatetags
.set_sensitive( False)
2477 new_label
= '%s (%s)' % ( self
.updatetags
.get_label(), _('needs python-eyed3') )
2478 self
.updatetags
.set_label( new_label
)
2481 self
.comboboxDeviceType
.set_active( 0)
2482 if gl
.config
.device_type
== 'ipod':
2483 self
.comboboxDeviceType
.set_active( 1)
2484 elif gl
.config
.device_type
== 'filesystem':
2485 self
.comboboxDeviceType
.set_active( 2)
2487 # setup cell renderers
2488 cellrenderer
= gtk
.CellRendererPixbuf()
2489 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, False)
2490 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
2491 cellrenderer
= gtk
.CellRendererText()
2492 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, True)
2493 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
2495 cellrenderer
= gtk
.CellRendererPixbuf()
2496 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, False)
2497 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
2498 cellrenderer
= gtk
.CellRendererText()
2499 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, True)
2500 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
2502 if not hasattr(self
, 'user_apps_reader'):
2503 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
2505 if gpodder
.interface
== gpodder
.GUI
:
2506 self
.user_apps_reader
.read()
2508 self
.comboAudioPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('audio'))
2509 index
= self
.find_active_audio_app()
2510 self
.comboAudioPlayerApp
.set_active(index
)
2511 self
.comboVideoPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('video'))
2512 index
= self
.find_active_video_app()
2513 self
.comboVideoPlayerApp
.set_active(index
)
2515 self
.ipodIcon
.set_from_icon_name( 'gnome-dev-ipod', gtk
.ICON_SIZE_BUTTON
)
2517 def update_mountpoint( self
, ipod
):
2518 if ipod
is None or ipod
.mount_point
is None:
2519 self
.iPodMountpoint
.set_label( '')
2521 self
.iPodMountpoint
.set_label( ipod
.mount_point
)
2523 def on_bluetooth_select_device_clicked(self
, widget
):
2524 # Stupid GTK doesn't provide us with a method to directly
2525 # edit the text of a gtk.Button without "destroying" the
2526 # image on it, so we dig into the button's widget tree and
2527 # get the gtk.Image and gtk.Label and edit the label directly.
2528 alignment
= self
.bluetooth_select_device
.get_child()
2529 hbox
= alignment
.get_child()
2530 (image
, label
) = hbox
.get_children()
2532 old_text
= label
.get_text()
2533 label
.set_text(_('Searching...'))
2534 self
.bluetooth_select_device
.set_sensitive(False)
2535 while gtk
.events_pending():
2536 gtk
.main_iteration(False)
2538 # FIXME: Make bluetooth device discovery threaded, so
2539 # the GUI doesn't freeze while we are searching for devices
2541 for name
, address
in util
.discover_bluetooth_devices():
2542 if self
.show_confirmation('Use this device as your bluetooth device?', name
):
2543 gl
.config
.bluetooth_device_name
= name
2544 gl
.config
.bluetooth_device_address
= address
2545 self
.bluetooth_device_name
.set_markup('<b>%s</b>'%gl
.config
.bluetooth_device_name
)
2549 self
.show_message('No more devices found', 'Scan finished')
2550 self
.bluetooth_select_device
.set_sensitive(True)
2551 label
.set_text(old_text
)
2553 def find_active_audio_app(self
):
2554 model
= self
.comboAudioPlayerApp
.get_model()
2555 iter = model
.get_iter_first()
2557 while iter is not None:
2558 command
= model
.get_value(iter, 1)
2559 if command
== self
.openApp
.get_text():
2561 iter = model
.iter_next(iter)
2563 # return last item = custom command
2566 def find_active_video_app( self
):
2567 model
= self
.comboVideoPlayerApp
.get_model()
2568 iter = model
.get_iter_first()
2570 while iter is not None:
2571 command
= model
.get_value(iter, 1)
2572 if command
== self
.openVideoApp
.get_text():
2574 iter = model
.iter_next(iter)
2576 # return last item = custom command
2579 def set_download_dir( self
, new_download_dir
, event
= None):
2580 gl
.downloaddir
= self
.chooserDownloadTo
.get_filename()
2581 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
2582 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'))
2587 def on_auto_update_feeds_toggled( self
, widget
, *args
):
2588 self
.auto_update_frequency
.set_sensitive(widget
.get_active())
2590 def on_display_tray_icon_toggled( self
, widget
, *args
):
2591 self
.enable_notifications
.set_sensitive(widget
.get_active())
2592 self
.minimize_to_tray
.set_sensitive(widget
.get_active())
2594 def on_cbCustomSyncName_toggled( self
, widget
, *args
):
2595 self
.entryCustomSyncName
.set_sensitive( widget
.get_active())
2597 def on_only_sync_not_played_toggled( self
, widget
, *args
):
2598 self
.delete_episodes_marked_played
.set_sensitive( widget
.get_active())
2599 if not widget
.get_active():
2600 self
.delete_episodes_marked_played
.set_active(False)
2602 def on_delete_episodes_marked_played_toggled( self
, widget
, *args
):
2603 if widget
.get_active() and self
.only_sync_not_played
.get_active():
2604 self
.on_sync_leave
.set_active(True)
2605 self
.on_sync_delete
.set_sensitive(not widget
.get_active())
2606 self
.on_sync_mark_played
.set_sensitive(not widget
.get_active())
2608 def on_btnCustomSyncNameHelp_clicked( self
, widget
):
2610 '<i>{episode.title}</i> -> <b>Interview with RMS</b>',
2611 '<i>{episode.basename}</i> -> <b>70908-interview-rms</b>',
2612 '<i>{episode.published}</i> -> <b>20070908</b>'
2616 _('You can specify a custom format string for the file names on your MP3 player here.'),
2617 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2618 '\n'.join( [ ' %s' % s
for s
in examples
])
2621 self
.show_message( '\n\n'.join( info
), _('Custom format strings'))
2623 def on_gPodderProperties_destroy(self
, widget
, *args
):
2624 self
.on_btnOK_clicked( widget
, *args
)
2626 def on_btnConfigEditor_clicked(self
, widget
, *args
):
2627 self
.on_btnOK_clicked(widget
, *args
)
2628 gPodderConfigEditor()
2630 def on_comboAudioPlayerApp_changed(self
, widget
, *args
):
2631 # find out which one
2632 iter = self
.comboAudioPlayerApp
.get_active_iter()
2633 model
= self
.comboAudioPlayerApp
.get_model()
2634 command
= model
.get_value( iter, 1)
2636 self
.openApp
.set_sensitive( True)
2638 self
.labelCustomCommand
.show()
2640 self
.openApp
.set_text( command
)
2641 self
.openApp
.set_sensitive( False)
2643 self
.labelCustomCommand
.hide()
2645 def on_comboVideoPlayerApp_changed(self
, widget
, *args
):
2646 # find out which one
2647 iter = self
.comboVideoPlayerApp
.get_active_iter()
2648 model
= self
.comboVideoPlayerApp
.get_model()
2649 command
= model
.get_value(iter, 1)
2651 self
.openVideoApp
.set_sensitive(True)
2652 self
.openVideoApp
.show()
2653 self
.label115
.show()
2655 self
.openVideoApp
.set_text(command
)
2656 self
.openVideoApp
.set_sensitive(False)
2657 self
.openVideoApp
.hide()
2658 self
.label115
.hide()
2660 def on_cbEnvironmentVariables_toggled(self
, widget
, *args
):
2661 sens
= not self
.cbEnvironmentVariables
.get_active()
2662 self
.httpProxy
.set_sensitive( sens
)
2663 self
.ftpProxy
.set_sensitive( sens
)
2665 def on_comboboxDeviceType_changed(self
, widget
, *args
):
2666 active_item
= self
.comboboxDeviceType
.get_active()
2669 sync_widgets
= ( self
.only_sync_not_played
, self
.labelSyncOptions
,
2670 self
.imageSyncOptions
, self
. separatorSyncOptions
,
2671 self
.on_sync_mark_played
, self
.on_sync_delete
,
2672 self
.on_sync_leave
, self
.label_after_sync
, self
.delete_episodes_marked_played
)
2673 for widget
in sync_widgets
:
2674 if active_item
== 0:
2680 ipod_widgets
= (self
.ipodLabel
, self
.btn_iPodMountpoint
,
2681 self
.ipod_write_gtkpod_extended
)
2682 for widget
in ipod_widgets
:
2683 if active_item
== 1:
2688 # filesystem-based MP3 player
2689 fs_widgets
= ( self
.filesystemLabel
, self
.btn_filesystemMountpoint
,
2690 self
.cbChannelSubfolder
, self
.cbCustomSyncName
,
2691 self
.entryCustomSyncName
, self
.btnCustomSyncNameHelp
)
2692 for widget
in fs_widgets
:
2693 if active_item
== 2:
2698 def on_btn_iPodMountpoint_clicked(self
, widget
, *args
):
2699 fs
= gtk
.FileChooserDialog( title
= _('Select iPod mountpoint'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
2700 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2701 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2702 fs
.set_current_folder(self
.iPodMountpoint
.get_label())
2703 if fs
.run() == gtk
.RESPONSE_OK
:
2704 self
.iPodMountpoint
.set_label( fs
.get_filename())
2707 def on_btn_FilesystemMountpoint_clicked(self
, widget
, *args
):
2708 fs
= gtk
.FileChooserDialog( title
= _('Select folder for MP3 player'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
2709 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2710 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2711 fs
.set_current_folder(self
.filesystemMountpoint
.get_label())
2712 if fs
.run() == gtk
.RESPONSE_OK
:
2713 self
.filesystemMountpoint
.set_label( fs
.get_filename())
2716 def on_btnOK_clicked(self
, widget
, *args
):
2717 gl
.config
.ipod_mount
= self
.iPodMountpoint
.get_label()
2718 gl
.config
.mp3_player_folder
= self
.filesystemMountpoint
.get_label()
2720 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
2721 new_download_dir
= self
.chooserDownloadTo
.get_filename()
2722 download_dir_size
= util
.calculate_size( gl
.downloaddir
)
2723 download_dir_size_string
= gl
.format_filesize( download_dir_size
)
2726 dlg
= gtk
.Dialog( _('Moving downloads folder'), self
.gPodderProperties
)
2727 dlg
.vbox
.set_spacing( 5)
2728 dlg
.set_border_width( 5)
2731 label
.set_line_wrap( True)
2732 label
.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils
.escape( gl
.downloaddir
), saxutils
.escape( new_download_dir
), ))
2733 myprogressbar
= gtk
.ProgressBar()
2735 # put it all together
2736 dlg
.vbox
.pack_start( label
)
2737 dlg
.vbox
.pack_end( myprogressbar
)
2741 self
.gPodderProperties
.hide_all()
2743 # hide action area and separator line
2744 dlg
.action_area
.hide()
2745 dlg
.set_has_separator( False)
2747 args
= ( new_download_dir
, event
, )
2749 thread
= Thread( target
= self
.set_download_dir
, args
= args
)
2752 while not event
.isSet():
2754 new_download_dir_size
= util
.calculate_size( new_download_dir
)
2756 new_download_dir_size
= 0
2757 if download_dir_size
> 0:
2758 fract
= (1.00*new_download_dir_size
) / (1.00*download_dir_size
)
2762 myprogressbar
.set_text( _('%s of %s') % ( gl
.format_filesize( new_download_dir_size
), download_dir_size_string
, ))
2764 myprogressbar
.set_text( _('Finishing... please wait.'))
2765 myprogressbar
.set_fraction(max(0.0,min(1.0,fract
)))
2767 while gtk
.events_pending():
2768 gtk
.main_iteration( False)
2772 device_type
= self
.comboboxDeviceType
.get_active()
2773 if device_type
== 0:
2774 gl
.config
.device_type
= 'none'
2775 elif device_type
== 1:
2776 gl
.config
.device_type
= 'ipod'
2777 elif device_type
== 2:
2778 gl
.config
.device_type
= 'filesystem'
2779 self
.gPodderProperties
.destroy()
2780 if self
.callback_finished
:
2781 self
.callback_finished()
2784 class gPodderEpisode(GladeWidget
):
2785 finger_friendly_widgets
= ['episode_description', 'btnCloseWindow', 'btnDownload',
2786 'btnCancel', 'btnSaveFile', 'btnPlay', 'btn_website']
2789 global WEB_BROWSER_ICON
2790 self
.image3166
.set_property('icon-name', WEB_BROWSER_ICON
)
2791 services
.download_status_manager
.register( 'list-changed', self
.on_download_status_changed
)
2792 services
.download_status_manager
.register( 'progress-detail', self
.on_download_status_progress
)
2794 self
.episode_title
.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils
.escape( self
.episode
.title
))
2796 if gpodder
.interface
== gpodder
.MAEMO
:
2797 # Hide the advanced prefs expander
2798 self
.expander1
.hide_all()
2800 b
= gtk
.TextBuffer()
2801 b
.set_text( strip( self
.episode
.description
))
2802 self
.episode_description
.set_buffer( b
)
2804 self
.gPodderEpisode
.set_title( self
.episode
.title
)
2805 self
.LabelDownloadLink
.set_text( self
.episode
.url
)
2806 self
.LabelWebsiteLink
.set_text( self
.episode
.link
)
2807 self
.labelPubDate
.set_text(self
.episode
.cute_pubdate())
2809 # Hide the "Go to website" button if we don't have a valid URL
2810 if self
.episode
.link
== self
.episode
.url
or not self
.episode
.link
:
2811 self
.btn_website
.hide_all()
2813 self
.channel_title
.set_markup( _('<i>from %s</i>') % saxutils
.escape( self
.channel
.title
))
2815 self
.hide_show_widgets()
2816 services
.download_status_manager
.request_progress_detail( self
.episode
.url
)
2818 def on_btnCancel_clicked( self
, widget
):
2819 services
.download_status_manager
.cancel_by_url( self
.episode
.url
)
2821 def on_gPodderEpisode_destroy( self
, widget
):
2822 services
.download_status_manager
.unregister( 'list-changed', self
.on_download_status_changed
)
2823 services
.download_status_manager
.unregister( 'progress-detail', self
.on_download_status_progress
)
2825 def on_download_status_changed( self
):
2826 self
.hide_show_widgets()
2828 def on_btn_website_clicked(self
, widget
):
2829 util
.open_website(self
.episode
.link
)
2831 def on_download_status_progress( self
, url
, progress
, speed
):
2832 if url
== self
.episode
.url
:
2833 progress
= float(min(100.0,max(0.0,progress
)))
2834 self
.progress_bar
.set_fraction(progress
/100.0)
2835 self
.progress_bar
.set_text( 'Downloading: %d%% (%s)' % ( progress
, speed
, ))
2837 def hide_show_widgets( self
):
2838 is_downloading
= services
.download_status_manager
.is_download_in_progress( self
.episode
.url
)
2840 self
.progress_bar
.show_all()
2841 self
.btnCancel
.show_all()
2842 self
.btnPlay
.hide_all()
2843 self
.btnSaveFile
.hide_all()
2844 self
.btnDownload
.hide_all()
2846 self
.progress_bar
.hide_all()
2847 self
.btnCancel
.hide_all()
2848 if os
.path
.exists( self
.episode
.local_filename()):
2849 self
.btnPlay
.show_all()
2850 self
.btnSaveFile
.show_all()
2851 self
.btnDownload
.hide_all()
2853 self
.btnPlay
.hide_all()
2854 self
.btnSaveFile
.hide_all()
2855 self
.btnDownload
.show_all()
2857 def on_btnCloseWindow_clicked(self
, widget
, *args
):
2858 self
.gPodderEpisode
.destroy()
2860 def on_btnDownload_clicked(self
, widget
, *args
):
2861 if self
.download_callback
:
2862 self
.download_callback()
2864 def on_btnPlay_clicked(self
, widget
, *args
):
2865 if self
.play_callback
:
2866 self
.play_callback()
2868 self
.gPodderEpisode
.destroy()
2870 def on_btnSaveFile_clicked(self
, widget
, *args
):
2871 self
.show_copy_dialog( src_filename
= self
.episode
.local_filename(), dst_filename
= self
.episode
.sync_filename())
2874 class gPodderSync(GladeWidget
):
2876 util
.idle_add(self
.imageSync
.set_from_icon_name
, 'gnome-dev-ipod', gtk
.ICON_SIZE_DIALOG
)
2878 self
.device
.register('progress', self
.on_progress
)
2879 self
.device
.register('sub-progress', self
.on_sub_progress
)
2880 self
.device
.register('status', self
.on_status
)
2881 self
.device
.register('done', self
.on_done
)
2883 def on_progress(self
, pos
, max):
2884 util
.idle_add(self
.progressbar
.set_fraction
, float(pos
)/float(max))
2885 util
.idle_add(self
.progressbar
.set_text
, _('%d of %d done') % (pos
, max))
2887 def on_sub_progress(self
, percentage
):
2888 util
.idle_add(self
.progressbar
.set_text
, _('Processing (%d%%)') % (percentage
))
2890 def on_status(self
, status
):
2891 util
.idle_add(self
.status_label
.set_markup
, '<i>%s</i>' % saxutils
.escape(status
))
2894 util
.idle_add(self
.gPodderSync
.destroy
)
2895 if not self
.gPodder
.minimized
:
2896 util
.idle_add(self
.notification
, _('Your device has been updated by gPodder.'), _('Operation finished'))
2898 def on_gPodderSync_destroy(self
, widget
, *args
):
2899 self
.device
.unregister('progress', self
.on_progress
)
2900 self
.device
.unregister('sub-progress', self
.on_sub_progress
)
2901 self
.device
.unregister('status', self
.on_status
)
2902 self
.device
.unregister('done', self
.on_done
)
2903 self
.device
.cancel()
2905 def on_cancel_button_clicked(self
, widget
, *args
):
2906 self
.device
.cancel()
2909 class gPodderOpmlLister(GladeWidget
):
2910 finger_friendly_widgets
= ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
2913 # initiate channels list
2915 self
.callback_for_channel
= None
2916 self
.callback_finished
= None
2918 if hasattr(self
, 'custom_title'):
2919 self
.gPodderOpmlLister
.set_title(self
.custom_title
)
2920 if hasattr(self
, 'hide_url_entry'):
2921 self
.hbox25
.hide_all()
2923 togglecell
= gtk
.CellRendererToggle()
2924 togglecell
.set_property( 'activatable', True)
2925 togglecell
.connect( 'toggled', self
.callback_edited
)
2926 togglecolumn
= gtk
.TreeViewColumn( '', togglecell
, active
=0)
2928 titlecell
= gtk
.CellRendererText()
2929 titlecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
2930 titlecolumn
= gtk
.TreeViewColumn(_('Podcast'), titlecell
, markup
=1)
2932 for itemcolumn
in ( togglecolumn
, titlecolumn
):
2933 self
.treeviewChannelChooser
.append_column( itemcolumn
)
2935 def callback_edited( self
, cell
, path
):
2936 model
= self
.treeviewChannelChooser
.get_model()
2938 url
= model
[path
][2]
2940 model
[path
][0] = not model
[path
][0]
2942 self
.channels
.append( url
)
2944 self
.channels
.remove( url
)
2946 self
.btnOK
.set_sensitive( bool(len(self
.channels
)))
2948 def thread_finished(self
, model
):
2949 self
.treeviewChannelChooser
.set_model(model
)
2950 self
.labelStatus
.set_label('')
2951 self
.btnDownloadOpml
.set_sensitive(True)
2952 self
.entryURL
.set_sensitive(True)
2953 self
.treeviewChannelChooser
.set_sensitive(True)
2956 def thread_func(self
):
2957 url
= self
.entryURL
.get_text()
2958 importer
= opml
.Importer(url
)
2959 model
= importer
.get_model()
2961 self
.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
2962 util
.idle_add(self
.thread_finished
, model
)
2964 def get_channels_from_url( self
, url
, callback_for_channel
= None, callback_finished
= None):
2965 if callback_for_channel
:
2966 self
.callback_for_channel
= callback_for_channel
2967 if callback_finished
:
2968 self
.callback_finished
= callback_finished
2969 self
.labelStatus
.set_label( _('Downloading, please wait...'))
2970 self
.entryURL
.set_text( url
)
2971 self
.btnDownloadOpml
.set_sensitive( False)
2972 self
.entryURL
.set_sensitive( False)
2973 self
.btnOK
.set_sensitive( False)
2974 self
.treeviewChannelChooser
.set_sensitive( False)
2975 Thread( target
= self
.thread_func
).start()
2977 def select_all( self
, value
):
2979 for row
in self
.treeviewChannelChooser
.get_model():
2982 self
.channels
.append(row
[2])
2983 self
.btnOK
.set_sensitive(bool(len(self
.channels
)))
2985 def on_gPodderOpmlLister_destroy(self
, widget
, *args
):
2988 def on_btnDownloadOpml_clicked(self
, widget
, *args
):
2989 self
.get_channels_from_url( self
.entryURL
.get_text())
2991 def on_btnSelectAll_clicked(self
, widget
, *args
):
2992 self
.select_all(True)
2994 def on_btnSelectNone_clicked(self
, widget
, *args
):
2995 self
.select_all(False)
2997 def on_btnOK_clicked(self
, widget
, *args
):
2998 self
.gPodderOpmlLister
.destroy()
3000 # add channels that have been selected
3001 for url
in self
.channels
:
3002 if self
.callback_for_channel
:
3003 self
.callback_for_channel( url
)
3005 if self
.callback_finished
:
3006 self
.callback_finished()
3008 def on_btnCancel_clicked(self
, widget
, *args
):
3009 self
.gPodderOpmlLister
.destroy()
3012 class gPodderEpisodeSelector( GladeWidget
):
3013 """Episode selection dialog
3015 Optional keyword arguments that modify the behaviour of this dialog:
3017 - callback: Function that takes 1 parameter which is a list of
3018 the selected episodes (or empty list when none selected)
3019 - episodes: List of episodes that are presented for selection
3020 - selected: (optional) List of boolean variables that define the
3021 default checked state for the given episodes
3022 - selected_default: (optional) The default boolean value for the
3023 checked state if no other value is set
3025 - columns: List of (name,caption) pairs for the columns, the name
3026 is the attribute name of the episode to be read from
3027 each episode object and the caption attribute is the
3028 text that appear as column caption
3029 (default is [('title','Episode'),])
3030 - title: (optional) The title of the window + heading
3031 - instructions: (optional) A one-line text describing what the
3032 user should select / what the selection is for
3033 - stock_ok_button: (optional) Will replace the "OK" button with
3034 another GTK+ stock item to be used for the
3035 affirmative button of the dialog (e.g. can
3036 be gtk.STOCK_DELETE when the episodes to be
3037 selected will be deleted after closing the
3039 - selection_buttons: (optional) A dictionary with labels as
3040 keys and callbacks as values; for each
3041 key a button will be generated, and when
3042 the button is clicked, the callback will
3043 be called for each episode and the return
3044 value of the callback (True or False) will
3045 be the new selected state of the episode
3046 - size_attribute: (optional) The name of an attribute of the
3047 supplied episode objects that can be used to
3048 calculate the size of an episode; set this to
3049 None if no total size calculation should be
3050 done (in cases where total size is useless)
3051 (default is 'length')
3054 finger_friendly_widgets
= ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3057 COLUMN_ADDITIONAL
= 1
3060 if not hasattr( self
, 'callback'):
3061 self
.callback
= None
3063 if not hasattr( self
, 'episodes'):
3066 if not hasattr( self
, 'size_attribute'):
3067 self
.size_attribute
= 'length'
3069 if not hasattr( self
, 'selection_buttons'):
3070 self
.selection_buttons
= {}
3072 if not hasattr( self
, 'selected_default'):
3073 self
.selected_default
= False
3075 if not hasattr( self
, 'selected'):
3076 self
.selected
= [self
.selected_default
]*len(self
.episodes
)
3078 if len(self
.selected
) < len(self
.episodes
):
3079 self
.selected
+= [self
.selected_default
]*(len(self
.episodes
)-len(self
.selected
))
3081 if not hasattr( self
, 'columns'):
3082 self
.columns
= ( ('title', _('Episode')), )
3084 if hasattr( self
, 'title'):
3085 self
.gPodderEpisodeSelector
.set_title( self
.title
)
3086 self
.labelHeading
.set_markup( '<b><big>%s</big></b>' % saxutils
.escape( self
.title
))
3088 if gpodder
.interface
== gpodder
.MAEMO
:
3089 self
.labelHeading
.hide()
3091 if hasattr( self
, 'instructions'):
3092 self
.labelInstructions
.set_text( self
.instructions
)
3093 self
.labelInstructions
.show_all()
3095 if hasattr(self
, 'stock_ok_button'):
3096 if self
.stock_ok_button
== 'gpodder-download':
3097 self
.btnOK
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_BUTTON
))
3098 self
.btnOK
.set_label(_('Download'))
3100 self
.btnOK
.set_label(self
.stock_ok_button
)
3101 self
.btnOK
.set_use_stock(True)
3103 toggle_cell
= gtk
.CellRendererToggle()
3104 toggle_cell
.connect( 'toggled', self
.toggle_cell_handler
)
3106 self
.treeviewEpisodes
.append_column( gtk
.TreeViewColumn( '', toggle_cell
, active
=self
.COLUMN_TOGGLE
))
3108 next_column
= self
.COLUMN_ADDITIONAL
3109 for name
, caption
in self
.columns
:
3110 renderer
= gtk
.CellRendererText()
3111 renderer
.set_property( 'ellipsize', pango
.ELLIPSIZE_END
)
3112 column
= gtk
.TreeViewColumn( caption
, renderer
, text
=next_column
)
3113 column
.set_resizable( True)
3114 # Only set "expand" on the first column (so more text is displayed there)
3115 column
.set_expand(next_column
== self
.COLUMN_ADDITIONAL
)
3116 self
.treeviewEpisodes
.append_column( column
)
3119 column_types
= [ gobject
.TYPE_BOOLEAN
] + [ gobject
.TYPE_STRING
] * len(self
.columns
)
3120 self
.model
= gtk
.ListStore( *column_types
)
3122 for index
, episode
in enumerate( self
.episodes
):
3123 row
= [ self
.selected
[index
] ]
3124 for name
, caption
in self
.columns
:
3125 if not hasattr(episode
, name
):
3126 log('Warning: Missing attribute "%s"', name
, sender
=self
)
3129 row
.append(getattr( episode
, name
))
3130 self
.model
.append( row
)
3132 for label
in self
.selection_buttons
:
3133 button
= gtk
.Button( label
)
3134 button
.connect('clicked', self
.custom_selection_button_clicked
, label
)
3135 self
.hboxButtons
.pack_start( button
, expand
= False)
3138 self
.treeviewEpisodes
.set_rules_hint( True)
3139 self
.treeviewEpisodes
.set_model( self
.model
)
3140 self
.treeviewEpisodes
.columns_autosize()
3141 self
.calculate_total_size()
3143 def calculate_total_size( self
):
3144 if self
.size_attribute
is not None:
3145 (total_size
, count
) = (0, 0)
3146 for index
, row
in enumerate( self
.model
):
3147 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
3149 total_size
+= int(getattr( self
.episodes
[index
], self
.size_attribute
))
3152 log( 'Cannot get size for %s', self
.episodes
[index
].title
, sender
= self
)
3157 text
.append(_('One episodes selected'))
3159 text
.append(_('%d episodes selected') % count
)
3160 text
.append(_('total size: %s') % gl
.format_filesize(total_size
))
3161 self
.labelTotalSize
.set_text(', '.join(text
))
3162 self
.btnOK
.set_sensitive(True)
3164 self
.labelTotalSize
.set_text(_('Nothing selected'))
3165 self
.btnOK
.set_sensitive(False)
3167 self
.btnOK
.set_sensitive(False)
3168 for index
, row
in enumerate(self
.model
):
3169 if self
.model
.get_value(row
.iter, self
.COLUMN_TOGGLE
) == True:
3170 self
.btnOK
.set_sensitive(True)
3172 self
.labelTotalSize
.set_text('')
3174 def toggle_cell_handler( self
, cell
, path
):
3175 model
= self
.treeviewEpisodes
.get_model()
3176 model
[path
][self
.COLUMN_TOGGLE
] = not model
[path
][self
.COLUMN_TOGGLE
]
3178 self
.calculate_total_size()
3180 def custom_selection_button_clicked(self
, button
, label
):
3181 callback
= self
.selection_buttons
[label
]
3183 for index
, row
in enumerate( self
.model
):
3184 new_value
= callback( self
.episodes
[index
])
3185 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, new_value
)
3187 self
.calculate_total_size()
3189 def on_btnCheckAll_clicked( self
, widget
):
3190 for row
in self
.model
:
3191 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, True)
3193 self
.calculate_total_size()
3195 def on_btnCheckNone_clicked( self
, widget
):
3196 for row
in self
.model
:
3197 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, False)
3199 self
.calculate_total_size()
3201 def get_selected_episodes( self
):
3202 selected_episodes
= []
3204 for index
, row
in enumerate( self
.model
):
3205 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
3206 selected_episodes
.append( self
.episodes
[index
])
3208 return selected_episodes
3210 def on_btnOK_clicked( self
, widget
):
3211 self
.gPodderEpisodeSelector
.destroy()
3212 if self
.callback
is not None:
3213 self
.callback( self
.get_selected_episodes())
3215 def on_btnCancel_clicked( self
, widget
):
3216 self
.gPodderEpisodeSelector
.destroy()
3217 if self
.callback
is not None:
3220 class gPodderConfigEditor(GladeWidget
):
3221 finger_friendly_widgets
= ['btnShowAll', 'btnClose', 'configeditor']
3224 name_column
= gtk
.TreeViewColumn(_('Variable'))
3225 name_renderer
= gtk
.CellRendererText()
3226 name_column
.pack_start(name_renderer
)
3227 name_column
.add_attribute(name_renderer
, 'text', 0)
3228 name_column
.add_attribute(name_renderer
, 'weight', 5)
3229 self
.configeditor
.append_column(name_column
)
3231 type_column
= gtk
.TreeViewColumn(_('Type'))
3232 type_renderer
= gtk
.CellRendererText()
3233 type_column
.pack_start(type_renderer
)
3234 type_column
.add_attribute(type_renderer
, 'text', 1)
3235 type_column
.add_attribute(type_renderer
, 'weight', 5)
3236 self
.configeditor
.append_column(type_column
)
3238 value_column
= gtk
.TreeViewColumn(_('Value'))
3239 value_renderer
= gtk
.CellRendererText()
3240 value_column
.pack_start(value_renderer
)
3241 value_column
.add_attribute(value_renderer
, 'text', 2)
3242 value_column
.add_attribute(value_renderer
, 'editable', 4)
3243 value_column
.add_attribute(value_renderer
, 'weight', 5)
3244 value_renderer
.connect('edited', self
.value_edited
)
3245 self
.configeditor
.append_column(value_column
)
3247 self
.model
= gl
.config
.model()
3248 self
.filter = self
.model
.filter_new()
3249 self
.filter.set_visible_func(self
.visible_func
)
3251 self
.configeditor
.set_model(self
.filter)
3252 self
.configeditor
.set_rules_hint(True)
3254 def visible_func(self
, model
, iter, user_data
=None):
3255 text
= self
.entryFilter
.get_text().lower()
3259 # either the variable name or its value
3260 return (text
in model
.get_value(iter, 0).lower() or
3261 text
in model
.get_value(iter, 2).lower())
3263 def value_edited(self
, renderer
, path
, new_text
):
3264 model
= self
.configeditor
.get_model()
3265 iter = model
.get_iter(path
)
3266 name
= model
.get_value(iter, 0)
3267 type_cute
= model
.get_value(iter, 1)
3269 if not gl
.config
.update_field(name
, new_text
):
3270 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
))
3272 def on_entryFilter_changed(self
, widget
):
3273 self
.filter.refilter()
3275 def on_btnShowAll_clicked(self
, widget
):
3276 self
.entryFilter
.set_text('')
3277 self
.entryFilter
.grab_focus()
3279 def on_configeditor_row_activated(self
, treeview
, path
, view_column
):
3280 model
= treeview
.get_model()
3281 it
= model
.get_iter(path
)
3282 field_name
= model
.get_value(it
, 0)
3283 field_type
= model
.get_value(it
, 3)
3285 # Flip the boolean config flag
3286 if field_type
== bool:
3287 gl
.config
.toggle_flag(field_name
)
3289 def on_btnClose_clicked(self
, widget
):
3290 self
.gPodderConfigEditor
.destroy()
3294 gobject
.threads_init()
3295 gtk
.window_set_default_icon_name( 'gpodder')