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 'Franz Seidl', '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', 'PhilF', '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
, util
.sanitize_filename(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
.state
== db
.STATE_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 # make sure gpod is available before even trying to sync
1672 if gl
.config
.device_type
== 'ipod' and not sync
.gpod_available
:
1673 title
= _('Cannot Sync To iPod')
1674 message
= _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
1675 self
.notification( message
, title
)
1677 Thread(target
=self
.sync_to_ipod_thread
, args
=(widget
, episodes
)).start()
1679 def sync_to_ipod_thread(self
, widget
, episodes
=None):
1680 device
= sync
.open_device()
1683 title
= _('No device configured')
1684 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1685 self
.notification(message
, title
)
1688 if not device
.open():
1689 title
= _('Cannot open device')
1690 message
= _('There has been an error opening your device.')
1691 self
.notification(message
, title
)
1694 gPodderSync(device
=device
, gPodder
=self
)
1696 self
.tray_icon
.set_synchronisation_device(device
)
1698 if episodes
is None:
1699 episodes_to_sync
= self
.get_all_episodes()
1700 device
.add_tracks(episodes_to_sync
)
1701 # 'only_sync_not_played' must be used or else all the played
1702 # tracks will be copied then immediately deleted
1703 if gl
.config
.mp3_player_delete_played
and gl
.config
.only_sync_not_played
:
1704 self
.ipod_delete_played(device
)
1706 device
.add_tracks(episodes
, force_played
=True)
1708 if not device
.close():
1709 title
= _('Error closing device')
1710 message
= _('There has been an error closing your device.')
1711 self
.notification(message
, title
)
1715 self
.tray_icon
.release_synchronisation_device()
1717 # update model for played state updates after sync
1718 for channel
in self
.channels
:
1719 util
.idle_add(channel
.update_model
)
1720 util
.idle_add(self
.updateComboBox
)
1722 def ipod_cleanup_callback(self
, device
, tracks
):
1723 title
= _('Delete podcasts from device?')
1724 message
= _('Do you really want to completely remove the selected episodes?')
1725 if len(tracks
) > 0 and self
.show_confirmation(message
, title
):
1726 device
.remove_tracks(tracks
)
1728 if not device
.close():
1729 title
= _('Error closing device')
1730 message
= _('There has been an error closing your device.')
1731 self
.show_message(message
, title
)
1734 def on_cleanup_ipod_activate(self
, widget
, *args
):
1736 ('title', _('Episode')),
1737 ('podcast', _('Podcast')),
1738 ('filesize', _('Size')),
1739 ('modified', _('Copied')),
1740 ('playcount', _('Play count')),
1741 ('released', _('Released')),
1744 device
= sync
.open_device()
1747 title
= _('No device configured')
1748 message
= _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1749 self
.show_message(message
, title
)
1752 if not device
.open():
1753 title
= _('Cannot open device')
1754 message
= _('There has been an error opening your device.')
1755 self
.show_message(message
, title
)
1758 gPodderSync(device
=device
, gPodder
=self
)
1760 tracks
= device
.get_all_tracks()
1762 remove_tracks_callback
= lambda tracks
: self
.ipod_cleanup_callback(device
, tracks
)
1764 for key
, caption
in columns
:
1765 want_this_column
= False
1766 for track
in tracks
:
1767 if getattr(track
, key
) is not None:
1768 want_this_column
= True
1771 if want_this_column
:
1772 wanted_columns
.append((key
, caption
))
1773 title
= _('Remove podcasts from device')
1774 instructions
= _('Select the podcast episodes you want to remove from your device.')
1775 gPodderEpisodeSelector(title
=title
, instructions
=instructions
, episodes
=tracks
, columns
=wanted_columns
, \
1776 stock_ok_button
=gtk
.STOCK_DELETE
, callback
=remove_tracks_callback
)
1778 title
= _('No files on device')
1779 message
= _('The devices contains no files to be removed.')
1780 self
.show_message(message
, title
)
1782 def show_hide_tray_icon(self
):
1783 if gl
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
1784 self
.tray_icon
= trayicon
.GPodderStatusIcon(self
, scalable_dir
)
1785 elif not gl
.config
.display_tray_icon
and self
.tray_icon
is not None:
1786 self
.tray_icon
.set_visible(False)
1788 self
.tray_icon
= None
1790 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
1791 self
.tray_icon
.set_visible(self
.minimized
)
1792 elif self
.tray_icon
:
1793 self
.tray_icon
.set_visible(True)
1795 def on_itemShowToolbar_activate(self
, widget
):
1796 gl
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
1798 def on_itemShowDescription_activate(self
, widget
):
1799 gl
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
1801 def update_item_device( self
):
1802 if gl
.config
.device_type
!= 'none':
1803 self
.itemDevice
.show_all()
1804 (label
,) = self
.itemDevice
.get_children()
1805 label
.set_text(gl
.get_device_name())
1807 self
.itemDevice
.hide_all()
1809 def properties_closed( self
):
1810 self
.show_hide_tray_icon()
1811 self
.update_item_device()
1812 self
.updateComboBox()
1814 def on_itemPreferences_activate(self
, widget
, *args
):
1815 if gpodder
.interface
== gpodder
.GUI
:
1816 gPodderProperties(callback_finished
=self
.properties_closed
, user_apps_reader
=self
.user_apps_reader
)
1818 gPodderMaemoPreferences()
1820 def on_itemAddChannel_activate(self
, widget
, *args
):
1821 if gpodder
.interface
== gpodder
.MAEMO
or not gl
.config
.show_podcast_url_entry
:
1822 gPodderAddPodcastDialog(url_callback
=self
.add_new_channel
)
1824 if self
.channelPaned
.get_position() < 200:
1825 self
.channelPaned
.set_position( 200)
1826 self
.entryAddChannel
.grab_focus()
1828 def on_itemEditChannel_activate(self
, widget
, *args
):
1829 if self
.active_channel
is None:
1830 title
= _('No podcast selected')
1831 message
= _('Please select a podcast in the podcasts list to edit.')
1832 self
.show_message( message
, title
)
1835 gPodderChannel(channel
=self
.active_channel
, callback_closed
=self
.updateComboBox
, callback_change_url
=self
.change_channel_url
)
1837 def change_channel_url(self
, old_url
, new_url
):
1840 channel
= podcastChannel
.load(url
=new_url
, create
=True)
1845 self
.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1848 for channel
in self
.channels
:
1849 if channel
.url
== old_url
:
1850 log('=> change channel url from %s to %s', old_url
, new_url
)
1851 old_save_dir
= channel
.save_dir
1852 channel
.url
= new_url
1853 new_save_dir
= channel
.save_dir
1854 log('old save dir=%s', old_save_dir
, sender
=self
)
1855 log('new save dir=%s', new_save_dir
, sender
=self
)
1856 files
= glob
.glob(os
.path
.join(old_save_dir
, '*'))
1857 log('moving %d files to %s', len(files
), new_save_dir
, sender
=self
)
1859 log('moving %s', file, sender
=self
)
1860 shutil
.move(file, new_save_dir
)
1862 os
.rmdir(old_save_dir
)
1864 log('Warning: cannot delete %s', old_save_dir
, sender
=self
)
1866 save_channels(self
.channels
)
1867 # update feed cache and select the podcast with the new URL afterwards
1868 self
.update_feed_cache(force_update
=False, select_url_afterwards
=new_url
)
1870 def on_itemRemoveChannel_activate(self
, widget
, *args
):
1872 if gpodder
.interface
== gpodder
.GUI
:
1873 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
1874 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
1875 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
1877 title
= _('Remove podcast and episodes?')
1878 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
1880 dialog
.set_title(title
)
1881 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
1883 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
1884 dialog
.vbox
.pack_start(cb_ask
)
1886 affirmative
= gtk
.RESPONSE_YES
1887 elif gpodder
.interface
== gpodder
.MAEMO
:
1888 cb_ask
= gtk
.CheckButton('') # dummy check button
1889 dialog
= hildon
.Note('confirmation', (self
.gPodder
, _('Do you really want to remove this podcast and all downloaded episodes?')))
1890 affirmative
= gtk
.RESPONSE_OK
1892 result
= dialog
.run()
1895 if result
== affirmative
:
1896 # delete downloaded episodes only if checkbox is unchecked
1897 if cb_ask
.get_active() == False:
1898 self
.active_channel
.remove_downloaded()
1900 log('Not removing downloaded episodes', sender
=self
)
1902 # only delete partial files if we do not have any downloads in progress
1903 delete_partial
= not services
.download_status_manager
.has_items()
1904 gl
.clean_up_downloads(delete_partial
)
1906 # get the URL of the podcast we want to select next
1907 position
= self
.channels
.index(self
.active_channel
)
1908 if position
== len(self
.channels
)-1:
1909 # this is the last podcast, so select the URL
1910 # of the item before this one (i.e. the "new last")
1911 select_url
= self
.channels
[position
-1].url
1913 # there is a podcast after the deleted one, so
1914 # we simply select the one that comes after it
1915 select_url
= self
.channels
[position
+1].url
1917 # Remove the channel
1918 self
.active_channel
.delete()
1919 self
.channels
.remove(self
.active_channel
)
1920 save_channels(self
.channels
)
1922 # Re-load the channels and select the desired new channel
1923 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
1925 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
1927 def get_opml_filter(self
):
1928 filter = gtk
.FileFilter()
1929 filter.add_pattern('*.opml')
1930 filter.add_pattern('*.xml')
1931 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
1934 def on_item_import_from_file_activate(self
, widget
, filename
=None):
1935 if filename
is None:
1936 if gpodder
.interface
== gpodder
.GUI
:
1937 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
1938 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1939 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
1940 elif gpodder
.interface
== gpodder
.MAEMO
:
1941 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
1942 dlg
.set_filter(self
.get_opml_filter())
1943 response
= dlg
.run()
1945 if response
== gtk
.RESPONSE_OK
:
1946 filename
= dlg
.get_filename()
1949 if filename
is not None:
1950 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
))
1952 def on_itemExportChannels_activate(self
, widget
, *args
):
1953 if not self
.channels
:
1954 title
= _('Nothing to export')
1955 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1956 self
.show_message( message
, title
)
1959 if gpodder
.interface
== gpodder
.GUI
:
1960 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
1961 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
1962 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
1963 elif gpodder
.interface
== gpodder
.MAEMO
:
1964 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
1965 dlg
.set_filter(self
.get_opml_filter())
1966 response
= dlg
.run()
1967 if response
== gtk
.RESPONSE_OK
:
1968 filename
= dlg
.get_filename()
1969 exporter
= opml
.Exporter( filename
)
1970 if not exporter
.write( self
.channels
):
1971 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1975 def on_itemImportChannels_activate(self
, widget
, *args
):
1976 gPodderOpmlLister().get_channels_from_url(gl
.config
.opml_url
, lambda url
: self
.add_new_channel(url
,False), lambda: self
.on_itemDownloadAllNew_activate(self
.gPodder
))
1978 def on_btnTransfer_clicked(self
, widget
, *args
):
1979 self
.on_treeAvailable_row_activated( widget
, args
)
1981 def on_homepage_activate(self
, widget
, *args
):
1982 util
.open_website(app_website
)
1984 def on_wiki_activate(self
, widget
, *args
):
1985 util
.open_website('http://wiki.gpodder.org/')
1987 def on_bug_tracker_activate(self
, widget
, *args
):
1988 util
.open_website('http://bugs.gpodder.org/')
1990 def on_itemAbout_activate(self
, widget
, *args
):
1991 dlg
= gtk
.AboutDialog()
1992 dlg
.set_name(app_name
.replace('p', 'P')) # gpodder->gPodder
1993 dlg
.set_version( app_version
)
1994 dlg
.set_copyright( app_copyright
)
1995 dlg
.set_website( app_website
)
1996 dlg
.set_translator_credits( _('translator-credits'))
1997 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
1999 if gpodder
.interface
== gpodder
.GUI
:
2000 # For the "GUI" version, we add some more
2001 # items to the about dialog (credits and logo)
2002 dlg
.set_authors(app_authors
)
2004 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file_at_size(scalable_dir
, 200, 200))
2010 def on_wNotebook_switch_page(self
, widget
, *args
):
2012 if gpodder
.interface
== gpodder
.MAEMO
:
2013 page
= self
.wNotebook
.get_nth_page(page_num
)
2014 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2015 if page_num
== 0 and self
.active_channel
is not None:
2016 self
.set_title(self
.active_channel
.title
)
2018 self
.set_title(tab_label
)
2020 self
.play_or_download()
2022 self
.toolDownload
.set_sensitive( False)
2023 self
.toolPlay
.set_sensitive( False)
2024 self
.toolTransfer
.set_sensitive( False)
2025 self
.toolCancel
.set_sensitive( services
.download_status_manager
.has_items())
2027 def on_treeChannels_row_activated(self
, widget
, *args
):
2028 self
.on_itemEditChannel_activate( self
.treeChannels
)
2030 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2031 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2033 if model
is not None and iter != None:
2034 id = model
.get_path( iter)[0]
2035 self
.active_channel
= self
.channels
[id]
2037 if gpodder
.interface
== gpodder
.MAEMO
:
2038 self
.set_title(self
.active_channel
.title
)
2039 self
.itemEditChannel
.show_all()
2040 self
.itemRemoveChannel
.show_all()
2042 self
.active_channel
= None
2043 self
.itemEditChannel
.hide_all()
2044 self
.itemRemoveChannel
.hide_all()
2046 self
.updateTreeView()
2048 def on_entryAddChannel_changed(self
, widget
, *args
):
2049 active
= self
.entryAddChannel
.get_text() not in ('', self
.ENTER_URL_TEXT
)
2050 self
.btnAddChannel
.set_sensitive( active
)
2052 def on_btnAddChannel_clicked(self
, widget
, *args
):
2053 url
= self
.entryAddChannel
.get_text()
2054 self
.entryAddChannel
.set_text('')
2055 self
.add_new_channel( url
)
2057 def on_btnEditChannel_clicked(self
, widget
, *args
):
2058 self
.on_itemEditChannel_activate( widget
, args
)
2060 def on_treeAvailable_row_activated(self
, widget
, *args
):
2062 selection
= self
.treeAvailable
.get_selection()
2063 selection_tuple
= selection
.get_selected_rows()
2064 transfer_files
= False
2067 if selection
.count_selected_rows() > 1:
2068 widget_to_send
= None
2069 show_message_dialog
= False
2071 widget_to_send
= widget
2072 show_message_dialog
= True
2074 if widget
.get_name() == 'itemTransferSelected' or widget
.get_name() == 'toolTransfer':
2075 transfer_files
= True
2077 services
.download_status_manager
.start_batch_mode()
2078 for apath
in selection_tuple
[1]:
2079 selection_iter
= self
.treeAvailable
.get_model().get_iter( apath
)
2080 url
= self
.treeAvailable
.get_model().get_value( selection_iter
, 0)
2083 episodes
.append( self
.active_channel
.find_episode( url
))
2085 self
.download_podcast_by_url( url
, show_message_dialog
, widget_to_send
)
2086 services
.download_status_manager
.end_batch_mode()
2088 if transfer_files
and len(episodes
):
2089 self
.on_sync_to_ipod_activate(None, episodes
)
2091 title
= _('Nothing selected')
2092 message
= _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
2093 self
.show_message( message
, title
)
2095 def on_btnDownload_clicked(self
, widget
, *args
):
2096 self
.on_treeAvailable_row_activated( widget
, args
)
2098 def on_treeAvailable_button_release_event(self
, widget
, *args
):
2099 self
.play_or_download()
2101 def auto_update_procedure(self
, first_run
=False):
2102 log('auto_update_procedure() got called', sender
=self
)
2103 if not first_run
and gl
.config
.auto_update_feeds
and self
.minimized
:
2104 self
.update_feed_cache(force_update
=True)
2106 next_update
= 60*1000*gl
.config
.auto_update_frequency
2107 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2109 def on_treeDownloads_row_activated(self
, widget
, *args
):
2112 if self
.wNotebook
.get_current_page() > 0:
2113 # Use the download list treeview + model
2114 ( tree
, column
) = ( self
.treeDownloads
, 3 )
2116 # Use the available podcasts treeview + model
2117 ( tree
, column
) = ( self
.treeAvailable
, 0 )
2119 selection
= tree
.get_selection()
2120 (model
, paths
) = selection
.get_selected_rows()
2122 url
= model
.get_value( model
.get_iter( path
), column
)
2123 cancel_urls
.append( url
)
2125 if len( cancel_urls
) == 0:
2126 log('Nothing selected.', sender
= self
)
2129 if len( cancel_urls
) == 1:
2130 title
= _('Cancel download?')
2131 message
= _("Cancelling this download will remove the partially downloaded file and stop the download.")
2133 title
= _('Cancel downloads?')
2134 message
= _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection
.count_selected_rows()
2136 if self
.show_confirmation( message
, title
):
2137 services
.download_status_manager
.start_batch_mode()
2138 for url
in cancel_urls
:
2139 services
.download_status_manager
.cancel_by_url( url
)
2140 services
.download_status_manager
.end_batch_mode()
2142 def on_btnCancelDownloadStatus_clicked(self
, widget
, *args
):
2143 self
.on_treeDownloads_row_activated( widget
, None)
2145 def on_btnCancelAll_clicked(self
, widget
, *args
):
2146 self
.treeDownloads
.get_selection().select_all()
2147 self
.on_treeDownloads_row_activated( self
.toolCancel
, None)
2148 self
.treeDownloads
.get_selection().unselect_all()
2150 def on_btnDownloadedExecute_clicked(self
, widget
, *args
):
2151 self
.on_treeAvailable_row_activated( widget
, args
)
2153 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2154 if self
.active_channel
is None:
2157 channel_url
= self
.active_channel
.url
2158 selection
= self
.treeAvailable
.get_selection()
2159 ( model
, paths
) = selection
.get_selected_rows()
2161 if selection
.count_selected_rows() == 0:
2162 log( 'Nothing selected - will not remove any downloaded episode.')
2165 if selection
.count_selected_rows() == 1:
2166 episode_title
= saxutils
.escape(model
.get_value(model
.get_iter(paths
[0]), 1))
2168 episode
= db
.load_episode(model
.get_value(model
.get_iter(paths
[0]), 0))
2169 if episode
['is_locked']:
2170 title
= _('%s is locked') % episode_title
2171 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2172 self
.notification(message
, title
)
2175 title
= _('Remove %s?') % episode_title
2176 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.")
2178 title
= _('Remove %d episodes?') % selection
.count_selected_rows()
2179 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.')
2183 episode
= db
.load_episode(model
.get_value(model
.get_iter(path
), 0))
2184 if episode
['is_locked']:
2187 if selection
.count_selected_rows() == locked_count
:
2188 title
= _('Episodes are locked')
2189 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2190 self
.notification(message
, title
)
2192 elif locked_count
> 0:
2193 title
= _('Remove %d out of %d episodes?') % (selection
.count_selected_rows() - locked_count
, selection
.count_selected_rows())
2194 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.')
2196 # if user confirms deletion, let's remove some stuff ;)
2197 if self
.show_confirmation( message
, title
):
2199 # iterate over the selection, see also on_treeDownloads_row_activated
2201 url
= model
.get_value( model
.get_iter( path
), 0)
2202 self
.active_channel
.delete_episode_by_url( url
)
2204 # now, clear local db cache so we can re-read it
2205 self
.updateComboBox()
2207 log( 'Error while deleting (some) downloads.')
2209 # only delete partial files if we do not have any downloads in progress
2210 delete_partial
= not services
.download_status_manager
.has_items()
2211 gl
.clean_up_downloads(delete_partial
)
2212 self
.active_channel
.force_update_tree_model()
2213 self
.updateTreeView()
2215 def on_key_press(self
, widget
, event
):
2216 # Currently, we only handle Maemo hardware keys here,
2217 # so if we are not a Maemo app, we don't do anything!
2218 if gpodder
.interface
!= gpodder
.MAEMO
:
2221 if event
.keyval
== gtk
.keysyms
.F6
:
2223 self
.window
.unfullscreen()
2225 self
.window
.fullscreen()
2226 if event
.keyval
== gtk
.keysyms
.Escape
:
2227 new_visibility
= not self
.vboxChannelNavigator
.get_property('visible')
2228 self
.vboxChannelNavigator
.set_property('visible', new_visibility
)
2229 self
.column_size
.set_visible(not new_visibility
)
2230 self
.column_released
.set_visible(not new_visibility
)
2233 if event
.keyval
== gtk
.keysyms
.F7
: #plus
2235 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
2239 selection
= self
.treeChannels
.get_selection()
2240 (model
, iter) = selection
.get_selected()
2241 selection
.select_path(((model
.get_path(iter)[0]+diff
)%len(model
),))
2242 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2244 def window_state_event(self
, widget
, event
):
2245 if event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_FULLSCREEN
:
2246 self
.fullscreen
= True
2248 self
.fullscreen
= False
2250 old_minimized
= self
.minimized
2252 if event
.new_window_state
& gtk
.gdk
.WINDOW_STATE_ICONIFIED
:
2253 self
.minimized
= True
2255 self
.minimized
= False
2257 if old_minimized
!= self
.minimized
and self
.tray_icon
:
2258 self
.gPodder
.set_skip_taskbar_hint(self
.minimized
)
2259 elif not self
.tray_icon
:
2260 self
.gPodder
.set_skip_taskbar_hint(False)
2262 if gl
.config
.minimize_to_tray
and self
.tray_icon
:
2263 self
.tray_icon
.set_visible(self
.minimized
)
2265 def uniconify_main_window(self
):
2267 self
.gPodder
.present()
2269 def iconify_main_window(self
):
2270 if not self
.minimized
:
2271 self
.gPodder
.iconify()
2273 class gPodderChannel(GladeWidget
):
2274 finger_friendly_widgets
= ['btn_website', 'btnOK', 'channel_description']
2277 global WEB_BROWSER_ICON
2278 self
.changed
= False
2279 self
.image3167
.set_property('icon-name', WEB_BROWSER_ICON
)
2280 self
.gPodderChannel
.set_title( self
.channel
.title
)
2281 self
.entryTitle
.set_text( self
.channel
.title
)
2282 self
.entryURL
.set_text( self
.channel
.url
)
2284 self
.LabelDownloadTo
.set_text( self
.channel
.save_dir
)
2285 self
.LabelWebsite
.set_text( self
.channel
.link
)
2287 self
.cbNoSync
.set_active( not self
.channel
.sync_to_devices
)
2288 self
.musicPlaylist
.set_text(self
.channel
.device_playlist_name
)
2289 if self
.channel
.username
:
2290 self
.FeedUsername
.set_text( self
.channel
.username
)
2291 if self
.channel
.password
:
2292 self
.FeedPassword
.set_text( self
.channel
.password
)
2294 services
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
2295 services
.cover_downloader
.request_cover(self
.channel
)
2297 # Hide the website button if we don't have a valid URL
2298 if not self
.channel
.link
:
2299 self
.btn_website
.hide_all()
2301 b
= gtk
.TextBuffer()
2302 b
.set_text( self
.channel
.description
)
2303 self
.channel_description
.set_buffer( b
)
2305 #Add Drag and Drop Support
2306 flags
= gtk
.DEST_DEFAULT_ALL
2307 targets
= [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2308 actions
= gtk
.gdk
.ACTION_DEFAULT | gtk
.gdk
.ACTION_COPY
2309 self
.vboxCoverEditor
.drag_dest_set( flags
, targets
, actions
)
2310 self
.vboxCoverEditor
.connect( 'drag_data_received', self
.drag_data_received
)
2312 def on_btn_website_clicked(self
, widget
):
2313 util
.open_website(self
.channel
.link
)
2315 def on_btnDownloadCover_clicked(self
, widget
):
2316 if gpodder
.interface
== gpodder
.GUI
:
2317 dlg
= gtk
.FileChooserDialog(title
=_('Select new podcast cover artwork'), parent
=self
.gPodderChannel
, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2318 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2319 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2320 elif gpodder
.interface
== gpodder
.MAEMO
:
2321 dlg
= hildon
.FileChooserDialog(self
.gPodderChannel
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2323 if dlg
.run() == gtk
.RESPONSE_OK
:
2325 services
.cover_downloader
.replace_cover(self
.channel
, url
)
2329 def on_btnClearCover_clicked(self
, widget
):
2330 services
.cover_downloader
.replace_cover(self
.channel
)
2332 def cover_download_finished(self
, channel_url
, pixbuf
):
2333 if pixbuf
is not None:
2334 self
.imgCover
.set_from_pixbuf(pixbuf
)
2335 self
.gPodderChannel
.show()
2337 def drag_data_received( self
, widget
, content
, x
, y
, sel
, ttype
, time
):
2338 files
= sel
.data
.strip().split('\n')
2340 self
.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2345 if file.startswith('file://') or file.startswith('http://'):
2346 services
.cover_downloader
.replace_cover(self
.channel
, file)
2349 self
.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2351 def on_gPodderChannel_destroy(self
, widget
, *args
):
2352 services
.cover_downloader
.unregister('cover-available', self
.cover_download_finished
)
2354 def on_btnOK_clicked(self
, widget
, *args
):
2355 entered_url
= self
.entryURL
.get_text()
2356 channel_url
= self
.channel
.url
2358 if entered_url
!= channel_url
:
2359 if self
.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils
.escape(entered_url
),), _('Really change URL?')):
2360 if hasattr(self
, 'callback_change_url'):
2361 self
.gPodderChannel
.hide_all()
2362 self
.callback_change_url(channel_url
, entered_url
)
2364 self
.channel
.sync_to_devices
= not self
.cbNoSync
.get_active()
2365 self
.channel
.device_playlist_name
= self
.musicPlaylist
.get_text()
2366 self
.channel
.set_custom_title( self
.entryTitle
.get_text())
2367 self
.channel
.username
= self
.FeedUsername
.get_text().strip()
2368 self
.channel
.password
= self
.FeedPassword
.get_text()
2371 self
.gPodderChannel
.destroy()
2372 self
.callback_closed()
2374 class gPodderAddPodcastDialog(GladeWidget
):
2375 finger_friendly_widgets
= ['btn_close', 'btn_add']
2378 if not hasattr(self
, 'url_callback'):
2379 log('No url callback set', sender
=self
)
2380 self
.url_callback
= None
2382 def on_btn_close_clicked(self
, widget
):
2383 self
.gPodderAddPodcastDialog
.destroy()
2385 def on_entry_url_changed(self
, widget
):
2386 self
.btn_add
.set_sensitive(self
.entry_url
.get_text().strip() != '')
2388 def on_btn_add_clicked(self
, widget
):
2389 url
= self
.entry_url
.get_text()
2390 self
.on_btn_close_clicked(widget
)
2391 if self
.url_callback
is not None:
2392 self
.url_callback(url
)
2395 class gPodderMaemoPreferences(GladeWidget
):
2396 finger_friendly_widgets
= ['btn_close', 'label128', 'label129', 'btn_advanced']
2399 gl
.config
.connect_gtk_togglebutton('update_on_startup', self
.update_on_startup
)
2400 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.show_tray_icon
)
2401 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.show_notifications
)
2402 gl
.config
.connect_gtk_togglebutton('on_quit_ask', self
.on_quit_ask
)
2404 self
.restart_required
= False
2405 self
.show_tray_icon
.connect('clicked', self
.on_restart_required
)
2406 self
.show_notifications
.connect('clicked', self
.on_restart_required
)
2408 def on_restart_required(self
, widget
):
2409 self
.restart_required
= True
2411 def on_btn_advanced_clicked(self
, widget
):
2412 self
.gPodderMaemoPreferences
.destroy()
2413 gPodderConfigEditor()
2415 def on_btn_close_clicked(self
, widget
):
2416 self
.gPodderMaemoPreferences
.destroy()
2417 if self
.restart_required
:
2418 self
.show_message(_('Please restart gPodder for the changes to take effect.'))
2421 class gPodderProperties(GladeWidget
):
2423 if not hasattr( self
, 'callback_finished'):
2424 self
.callback_finished
= None
2426 if gpodder
.interface
== gpodder
.MAEMO
:
2427 self
.table13
.hide_all() # bluetooth
2428 self
.table5
.hide_all() # player
2429 self
.table6
.hide_all() # bittorrent
2430 self
.gPodderProperties
.fullscreen()
2432 gl
.config
.connect_gtk_editable( 'http_proxy', self
.httpProxy
)
2433 gl
.config
.connect_gtk_editable( 'ftp_proxy', self
.ftpProxy
)
2434 gl
.config
.connect_gtk_editable( 'player', self
.openApp
)
2435 gl
.config
.connect_gtk_editable('videoplayer', self
.openVideoApp
)
2436 gl
.config
.connect_gtk_editable( 'custom_sync_name', self
.entryCustomSyncName
)
2437 gl
.config
.connect_gtk_togglebutton( 'custom_sync_name_enabled', self
.cbCustomSyncName
)
2438 gl
.config
.connect_gtk_togglebutton( 'auto_download_when_minimized', self
.downloadnew
)
2439 gl
.config
.connect_gtk_togglebutton( 'use_gnome_bittorrent', self
.radio_gnome_bittorrent
)
2440 gl
.config
.connect_gtk_togglebutton( 'update_on_startup', self
.updateonstartup
)
2441 gl
.config
.connect_gtk_togglebutton( 'only_sync_not_played', self
.only_sync_not_played
)
2442 gl
.config
.connect_gtk_togglebutton( 'fssync_channel_subfolders', self
.cbChannelSubfolder
)
2443 gl
.config
.connect_gtk_togglebutton( 'on_sync_mark_played', self
.on_sync_mark_played
)
2444 gl
.config
.connect_gtk_togglebutton( 'on_sync_delete', self
.on_sync_delete
)
2445 gl
.config
.connect_gtk_togglebutton( 'proxy_use_environment', self
.cbEnvironmentVariables
)
2446 gl
.config
.connect_gtk_filechooser( 'bittorrent_dir', self
.chooserBitTorrentTo
)
2447 gl
.config
.connect_gtk_spinbutton('episode_old_age', self
.episode_old_age
)
2448 gl
.config
.connect_gtk_togglebutton('auto_remove_old_episodes', self
.auto_remove_old_episodes
)
2449 gl
.config
.connect_gtk_togglebutton('auto_update_feeds', self
.auto_update_feeds
)
2450 gl
.config
.connect_gtk_spinbutton('auto_update_frequency', self
.auto_update_frequency
)
2451 gl
.config
.connect_gtk_togglebutton('display_tray_icon', self
.display_tray_icon
)
2452 gl
.config
.connect_gtk_togglebutton('minimize_to_tray', self
.minimize_to_tray
)
2453 gl
.config
.connect_gtk_togglebutton('enable_notifications', self
.enable_notifications
)
2454 gl
.config
.connect_gtk_togglebutton('start_iconified', self
.start_iconified
)
2455 gl
.config
.connect_gtk_togglebutton('bluetooth_enabled', self
.bluetooth_enabled
)
2456 gl
.config
.connect_gtk_togglebutton('bluetooth_ask_always', self
.bluetooth_ask_always
)
2457 gl
.config
.connect_gtk_togglebutton('bluetooth_ask_never', self
.bluetooth_ask_never
)
2458 gl
.config
.connect_gtk_togglebutton('bluetooth_use_converter', self
.bluetooth_use_converter
)
2459 gl
.config
.connect_gtk_filechooser( 'bluetooth_converter', self
.bluetooth_converter
, is_for_files
=True)
2460 gl
.config
.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self
.ipod_write_gtkpod_extended
)
2461 gl
.config
.connect_gtk_togglebutton('mp3_player_delete_played', self
.delete_episodes_marked_played
)
2463 self
.enable_notifications
.set_sensitive(self
.display_tray_icon
.get_active())
2464 self
.minimize_to_tray
.set_sensitive(self
.display_tray_icon
.get_active())
2466 self
.entryCustomSyncName
.set_sensitive( self
.cbCustomSyncName
.get_active())
2468 self
.radio_gnome_bittorrent
.set_active(gl
.config
.use_gnome_bittorrent
)
2469 self
.radio_copy_torrents
.set_active(not gl
.config
.use_gnome_bittorrent
)
2471 self
.iPodMountpoint
.set_label( gl
.config
.ipod_mount
)
2472 self
.filesystemMountpoint
.set_label( gl
.config
.mp3_player_folder
)
2473 self
.bluetooth_device_name
.set_markup('<b>%s</b>'%gl
.config
.bluetooth_device_name
)
2474 self
.chooserDownloadTo
.set_current_folder(gl
.downloaddir
)
2476 self
.on_sync_delete
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2477 self
.on_sync_mark_played
.set_sensitive(not self
.delete_episodes_marked_played
.get_active())
2479 if tagging_supported():
2480 gl
.config
.connect_gtk_togglebutton( 'update_tags', self
.updatetags
)
2482 self
.updatetags
.set_sensitive( False)
2483 new_label
= '%s (%s)' % ( self
.updatetags
.get_label(), _('needs python-eyed3') )
2484 self
.updatetags
.set_label( new_label
)
2487 self
.comboboxDeviceType
.set_active( 0)
2488 if gl
.config
.device_type
== 'ipod':
2489 self
.comboboxDeviceType
.set_active( 1)
2490 elif gl
.config
.device_type
== 'filesystem':
2491 self
.comboboxDeviceType
.set_active( 2)
2493 # setup cell renderers
2494 cellrenderer
= gtk
.CellRendererPixbuf()
2495 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, False)
2496 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
2497 cellrenderer
= gtk
.CellRendererText()
2498 self
.comboAudioPlayerApp
.pack_start(cellrenderer
, True)
2499 self
.comboAudioPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
2501 cellrenderer
= gtk
.CellRendererPixbuf()
2502 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, False)
2503 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'pixbuf', 2)
2504 cellrenderer
= gtk
.CellRendererText()
2505 self
.comboVideoPlayerApp
.pack_start(cellrenderer
, True)
2506 self
.comboVideoPlayerApp
.add_attribute(cellrenderer
, 'markup', 0)
2508 if not hasattr(self
, 'user_apps_reader'):
2509 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
2511 if gpodder
.interface
== gpodder
.GUI
:
2512 self
.user_apps_reader
.read()
2514 self
.comboAudioPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('audio'))
2515 index
= self
.find_active_audio_app()
2516 self
.comboAudioPlayerApp
.set_active(index
)
2517 self
.comboVideoPlayerApp
.set_model(self
.user_apps_reader
.get_applications_as_model('video'))
2518 index
= self
.find_active_video_app()
2519 self
.comboVideoPlayerApp
.set_active(index
)
2521 self
.ipodIcon
.set_from_icon_name( 'gnome-dev-ipod', gtk
.ICON_SIZE_BUTTON
)
2523 def update_mountpoint( self
, ipod
):
2524 if ipod
is None or ipod
.mount_point
is None:
2525 self
.iPodMountpoint
.set_label( '')
2527 self
.iPodMountpoint
.set_label( ipod
.mount_point
)
2529 def on_bluetooth_select_device_clicked(self
, widget
):
2530 # Stupid GTK doesn't provide us with a method to directly
2531 # edit the text of a gtk.Button without "destroying" the
2532 # image on it, so we dig into the button's widget tree and
2533 # get the gtk.Image and gtk.Label and edit the label directly.
2534 alignment
= self
.bluetooth_select_device
.get_child()
2535 hbox
= alignment
.get_child()
2536 (image
, label
) = hbox
.get_children()
2538 old_text
= label
.get_text()
2539 label
.set_text(_('Searching...'))
2540 self
.bluetooth_select_device
.set_sensitive(False)
2541 while gtk
.events_pending():
2542 gtk
.main_iteration(False)
2544 # FIXME: Make bluetooth device discovery threaded, so
2545 # the GUI doesn't freeze while we are searching for devices
2547 for name
, address
in util
.discover_bluetooth_devices():
2548 if self
.show_confirmation('Use this device as your bluetooth device?', name
):
2549 gl
.config
.bluetooth_device_name
= name
2550 gl
.config
.bluetooth_device_address
= address
2551 self
.bluetooth_device_name
.set_markup('<b>%s</b>'%gl
.config
.bluetooth_device_name
)
2555 self
.show_message('No more devices found', 'Scan finished')
2556 self
.bluetooth_select_device
.set_sensitive(True)
2557 label
.set_text(old_text
)
2559 def find_active_audio_app(self
):
2560 model
= self
.comboAudioPlayerApp
.get_model()
2561 iter = model
.get_iter_first()
2563 while iter is not None:
2564 command
= model
.get_value(iter, 1)
2565 if command
== self
.openApp
.get_text():
2567 iter = model
.iter_next(iter)
2569 # return last item = custom command
2572 def find_active_video_app( self
):
2573 model
= self
.comboVideoPlayerApp
.get_model()
2574 iter = model
.get_iter_first()
2576 while iter is not None:
2577 command
= model
.get_value(iter, 1)
2578 if command
== self
.openVideoApp
.get_text():
2580 iter = model
.iter_next(iter)
2582 # return last item = custom command
2585 def set_download_dir( self
, new_download_dir
, event
= None):
2586 gl
.downloaddir
= self
.chooserDownloadTo
.get_filename()
2587 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
2588 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'))
2593 def on_auto_update_feeds_toggled( self
, widget
, *args
):
2594 self
.auto_update_frequency
.set_sensitive(widget
.get_active())
2596 def on_display_tray_icon_toggled( self
, widget
, *args
):
2597 self
.enable_notifications
.set_sensitive(widget
.get_active())
2598 self
.minimize_to_tray
.set_sensitive(widget
.get_active())
2600 def on_cbCustomSyncName_toggled( self
, widget
, *args
):
2601 self
.entryCustomSyncName
.set_sensitive( widget
.get_active())
2603 def on_only_sync_not_played_toggled( self
, widget
, *args
):
2604 self
.delete_episodes_marked_played
.set_sensitive( widget
.get_active())
2605 if not widget
.get_active():
2606 self
.delete_episodes_marked_played
.set_active(False)
2608 def on_delete_episodes_marked_played_toggled( self
, widget
, *args
):
2609 if widget
.get_active() and self
.only_sync_not_played
.get_active():
2610 self
.on_sync_leave
.set_active(True)
2611 self
.on_sync_delete
.set_sensitive(not widget
.get_active())
2612 self
.on_sync_mark_played
.set_sensitive(not widget
.get_active())
2614 def on_btnCustomSyncNameHelp_clicked( self
, widget
):
2616 '<i>{episode.title}</i> -> <b>Interview with RMS</b>',
2617 '<i>{episode.basename}</i> -> <b>70908-interview-rms</b>',
2618 '<i>{episode.published}</i> -> <b>20070908</b>'
2622 _('You can specify a custom format string for the file names on your MP3 player here.'),
2623 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2624 '\n'.join( [ ' %s' % s
for s
in examples
])
2627 self
.show_message( '\n\n'.join( info
), _('Custom format strings'))
2629 def on_gPodderProperties_destroy(self
, widget
, *args
):
2630 self
.on_btnOK_clicked( widget
, *args
)
2632 def on_btnConfigEditor_clicked(self
, widget
, *args
):
2633 self
.on_btnOK_clicked(widget
, *args
)
2634 gPodderConfigEditor()
2636 def on_comboAudioPlayerApp_changed(self
, widget
, *args
):
2637 # find out which one
2638 iter = self
.comboAudioPlayerApp
.get_active_iter()
2639 model
= self
.comboAudioPlayerApp
.get_model()
2640 command
= model
.get_value( iter, 1)
2642 self
.openApp
.set_sensitive( True)
2644 self
.labelCustomCommand
.show()
2646 self
.openApp
.set_text( command
)
2647 self
.openApp
.set_sensitive( False)
2649 self
.labelCustomCommand
.hide()
2651 def on_comboVideoPlayerApp_changed(self
, widget
, *args
):
2652 # find out which one
2653 iter = self
.comboVideoPlayerApp
.get_active_iter()
2654 model
= self
.comboVideoPlayerApp
.get_model()
2655 command
= model
.get_value(iter, 1)
2657 self
.openVideoApp
.set_sensitive(True)
2658 self
.openVideoApp
.show()
2659 self
.label115
.show()
2661 self
.openVideoApp
.set_text(command
)
2662 self
.openVideoApp
.set_sensitive(False)
2663 self
.openVideoApp
.hide()
2664 self
.label115
.hide()
2666 def on_cbEnvironmentVariables_toggled(self
, widget
, *args
):
2667 sens
= not self
.cbEnvironmentVariables
.get_active()
2668 self
.httpProxy
.set_sensitive( sens
)
2669 self
.ftpProxy
.set_sensitive( sens
)
2671 def on_comboboxDeviceType_changed(self
, widget
, *args
):
2672 active_item
= self
.comboboxDeviceType
.get_active()
2675 sync_widgets
= ( self
.only_sync_not_played
, self
.labelSyncOptions
,
2676 self
.imageSyncOptions
, self
. separatorSyncOptions
,
2677 self
.on_sync_mark_played
, self
.on_sync_delete
,
2678 self
.on_sync_leave
, self
.label_after_sync
, self
.delete_episodes_marked_played
)
2679 for widget
in sync_widgets
:
2680 if active_item
== 0:
2686 ipod_widgets
= (self
.ipodLabel
, self
.btn_iPodMountpoint
,
2687 self
.ipod_write_gtkpod_extended
)
2688 for widget
in ipod_widgets
:
2689 if active_item
== 1:
2694 # filesystem-based MP3 player
2695 fs_widgets
= ( self
.filesystemLabel
, self
.btn_filesystemMountpoint
,
2696 self
.cbChannelSubfolder
, self
.cbCustomSyncName
,
2697 self
.entryCustomSyncName
, self
.btnCustomSyncNameHelp
)
2698 for widget
in fs_widgets
:
2699 if active_item
== 2:
2704 def on_btn_iPodMountpoint_clicked(self
, widget
, *args
):
2705 fs
= gtk
.FileChooserDialog( title
= _('Select iPod mountpoint'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
2706 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2707 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2708 fs
.set_current_folder(self
.iPodMountpoint
.get_label())
2709 if fs
.run() == gtk
.RESPONSE_OK
:
2710 self
.iPodMountpoint
.set_label( fs
.get_filename())
2713 def on_btn_FilesystemMountpoint_clicked(self
, widget
, *args
):
2714 fs
= gtk
.FileChooserDialog( title
= _('Select folder for MP3 player'), action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
2715 fs
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2716 fs
.add_button( gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2717 fs
.set_current_folder(self
.filesystemMountpoint
.get_label())
2718 if fs
.run() == gtk
.RESPONSE_OK
:
2719 self
.filesystemMountpoint
.set_label( fs
.get_filename())
2722 def on_btnOK_clicked(self
, widget
, *args
):
2723 gl
.config
.ipod_mount
= self
.iPodMountpoint
.get_label()
2724 gl
.config
.mp3_player_folder
= self
.filesystemMountpoint
.get_label()
2726 if gl
.downloaddir
!= self
.chooserDownloadTo
.get_filename():
2727 new_download_dir
= self
.chooserDownloadTo
.get_filename()
2728 download_dir_size
= util
.calculate_size( gl
.downloaddir
)
2729 download_dir_size_string
= gl
.format_filesize( download_dir_size
)
2732 dlg
= gtk
.Dialog( _('Moving downloads folder'), self
.gPodderProperties
)
2733 dlg
.vbox
.set_spacing( 5)
2734 dlg
.set_border_width( 5)
2737 label
.set_line_wrap( True)
2738 label
.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils
.escape( gl
.downloaddir
), saxutils
.escape( new_download_dir
), ))
2739 myprogressbar
= gtk
.ProgressBar()
2741 # put it all together
2742 dlg
.vbox
.pack_start( label
)
2743 dlg
.vbox
.pack_end( myprogressbar
)
2747 self
.gPodderProperties
.hide_all()
2749 # hide action area and separator line
2750 dlg
.action_area
.hide()
2751 dlg
.set_has_separator( False)
2753 args
= ( new_download_dir
, event
, )
2755 thread
= Thread( target
= self
.set_download_dir
, args
= args
)
2758 while not event
.isSet():
2760 new_download_dir_size
= util
.calculate_size( new_download_dir
)
2762 new_download_dir_size
= 0
2763 if download_dir_size
> 0:
2764 fract
= (1.00*new_download_dir_size
) / (1.00*download_dir_size
)
2768 myprogressbar
.set_text( _('%s of %s') % ( gl
.format_filesize( new_download_dir_size
), download_dir_size_string
, ))
2770 myprogressbar
.set_text( _('Finishing... please wait.'))
2771 myprogressbar
.set_fraction(max(0.0,min(1.0,fract
)))
2773 while gtk
.events_pending():
2774 gtk
.main_iteration( False)
2778 device_type
= self
.comboboxDeviceType
.get_active()
2779 if device_type
== 0:
2780 gl
.config
.device_type
= 'none'
2781 elif device_type
== 1:
2782 gl
.config
.device_type
= 'ipod'
2783 elif device_type
== 2:
2784 gl
.config
.device_type
= 'filesystem'
2785 self
.gPodderProperties
.destroy()
2786 if self
.callback_finished
:
2787 self
.callback_finished()
2790 class gPodderEpisode(GladeWidget
):
2791 finger_friendly_widgets
= ['episode_description', 'btnCloseWindow', 'btnDownload',
2792 'btnCancel', 'btnSaveFile', 'btnPlay', 'btn_website']
2795 global WEB_BROWSER_ICON
2796 self
.image3166
.set_property('icon-name', WEB_BROWSER_ICON
)
2797 services
.download_status_manager
.register( 'list-changed', self
.on_download_status_changed
)
2798 services
.download_status_manager
.register( 'progress-detail', self
.on_download_status_progress
)
2800 self
.episode_title
.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils
.escape( self
.episode
.title
))
2802 if gpodder
.interface
== gpodder
.MAEMO
:
2803 # Hide the advanced prefs expander
2804 self
.expander1
.hide_all()
2806 b
= gtk
.TextBuffer()
2807 b
.set_text( strip( self
.episode
.description
))
2808 self
.episode_description
.set_buffer( b
)
2810 self
.gPodderEpisode
.set_title( self
.episode
.title
)
2811 self
.LabelDownloadLink
.set_text( self
.episode
.url
)
2812 self
.LabelWebsiteLink
.set_text( self
.episode
.link
)
2813 self
.labelPubDate
.set_text(self
.episode
.cute_pubdate())
2815 # Hide the "Go to website" button if we don't have a valid URL
2816 if self
.episode
.link
== self
.episode
.url
or not self
.episode
.link
:
2817 self
.btn_website
.hide_all()
2819 self
.channel_title
.set_markup( _('<i>from %s</i>') % saxutils
.escape( self
.channel
.title
))
2821 self
.hide_show_widgets()
2822 services
.download_status_manager
.request_progress_detail( self
.episode
.url
)
2824 def on_btnCancel_clicked( self
, widget
):
2825 services
.download_status_manager
.cancel_by_url( self
.episode
.url
)
2827 def on_gPodderEpisode_destroy( self
, widget
):
2828 services
.download_status_manager
.unregister( 'list-changed', self
.on_download_status_changed
)
2829 services
.download_status_manager
.unregister( 'progress-detail', self
.on_download_status_progress
)
2831 def on_download_status_changed( self
):
2832 self
.hide_show_widgets()
2834 def on_btn_website_clicked(self
, widget
):
2835 util
.open_website(self
.episode
.link
)
2837 def on_download_status_progress( self
, url
, progress
, speed
):
2838 if url
== self
.episode
.url
:
2839 progress
= float(min(100.0,max(0.0,progress
)))
2840 self
.progress_bar
.set_fraction(progress
/100.0)
2841 self
.progress_bar
.set_text( 'Downloading: %d%% (%s)' % ( progress
, speed
, ))
2843 def hide_show_widgets( self
):
2844 is_downloading
= services
.download_status_manager
.is_download_in_progress( self
.episode
.url
)
2846 self
.progress_bar
.show_all()
2847 self
.btnCancel
.show_all()
2848 self
.btnPlay
.hide_all()
2849 self
.btnSaveFile
.hide_all()
2850 self
.btnDownload
.hide_all()
2852 self
.progress_bar
.hide_all()
2853 self
.btnCancel
.hide_all()
2854 if os
.path
.exists( self
.episode
.local_filename()):
2855 self
.btnPlay
.show_all()
2856 self
.btnSaveFile
.show_all()
2857 self
.btnDownload
.hide_all()
2859 self
.btnPlay
.hide_all()
2860 self
.btnSaveFile
.hide_all()
2861 self
.btnDownload
.show_all()
2863 def on_btnCloseWindow_clicked(self
, widget
, *args
):
2864 self
.gPodderEpisode
.destroy()
2866 def on_btnDownload_clicked(self
, widget
, *args
):
2867 if self
.download_callback
:
2868 self
.download_callback()
2870 def on_btnPlay_clicked(self
, widget
, *args
):
2871 if self
.play_callback
:
2872 self
.play_callback()
2874 self
.gPodderEpisode
.destroy()
2876 def on_btnSaveFile_clicked(self
, widget
, *args
):
2877 self
.show_copy_dialog( src_filename
= self
.episode
.local_filename(), dst_filename
= self
.episode
.sync_filename())
2880 class gPodderSync(GladeWidget
):
2882 util
.idle_add(self
.imageSync
.set_from_icon_name
, 'gnome-dev-ipod', gtk
.ICON_SIZE_DIALOG
)
2884 self
.device
.register('progress', self
.on_progress
)
2885 self
.device
.register('sub-progress', self
.on_sub_progress
)
2886 self
.device
.register('status', self
.on_status
)
2887 self
.device
.register('done', self
.on_done
)
2889 def on_progress(self
, pos
, max):
2890 util
.idle_add(self
.progressbar
.set_fraction
, float(pos
)/float(max))
2891 util
.idle_add(self
.progressbar
.set_text
, _('%d of %d done') % (pos
, max))
2893 def on_sub_progress(self
, percentage
):
2894 util
.idle_add(self
.progressbar
.set_text
, _('Processing (%d%%)') % (percentage
))
2896 def on_status(self
, status
):
2897 util
.idle_add(self
.status_label
.set_markup
, '<i>%s</i>' % saxutils
.escape(status
))
2900 util
.idle_add(self
.gPodderSync
.destroy
)
2901 if not self
.gPodder
.minimized
:
2902 util
.idle_add(self
.notification
, _('Your device has been updated by gPodder.'), _('Operation finished'))
2904 def on_gPodderSync_destroy(self
, widget
, *args
):
2905 self
.device
.unregister('progress', self
.on_progress
)
2906 self
.device
.unregister('sub-progress', self
.on_sub_progress
)
2907 self
.device
.unregister('status', self
.on_status
)
2908 self
.device
.unregister('done', self
.on_done
)
2909 self
.device
.cancel()
2911 def on_cancel_button_clicked(self
, widget
, *args
):
2912 self
.device
.cancel()
2915 class gPodderOpmlLister(GladeWidget
):
2916 finger_friendly_widgets
= ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
2919 # initiate channels list
2921 self
.callback_for_channel
= None
2922 self
.callback_finished
= None
2924 if hasattr(self
, 'custom_title'):
2925 self
.gPodderOpmlLister
.set_title(self
.custom_title
)
2926 if hasattr(self
, 'hide_url_entry'):
2927 self
.hbox25
.hide_all()
2929 togglecell
= gtk
.CellRendererToggle()
2930 togglecell
.set_property( 'activatable', True)
2931 togglecell
.connect( 'toggled', self
.callback_edited
)
2932 togglecolumn
= gtk
.TreeViewColumn( '', togglecell
, active
=0)
2934 titlecell
= gtk
.CellRendererText()
2935 titlecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
2936 titlecolumn
= gtk
.TreeViewColumn(_('Podcast'), titlecell
, markup
=1)
2938 for itemcolumn
in ( togglecolumn
, titlecolumn
):
2939 self
.treeviewChannelChooser
.append_column( itemcolumn
)
2941 def callback_edited( self
, cell
, path
):
2942 model
= self
.treeviewChannelChooser
.get_model()
2944 url
= model
[path
][2]
2946 model
[path
][0] = not model
[path
][0]
2948 self
.channels
.append( url
)
2950 self
.channels
.remove( url
)
2952 self
.btnOK
.set_sensitive( bool(len(self
.channels
)))
2954 def thread_finished(self
, model
):
2955 self
.treeviewChannelChooser
.set_model(model
)
2956 self
.labelStatus
.set_label('')
2957 self
.btnDownloadOpml
.set_sensitive(True)
2958 self
.entryURL
.set_sensitive(True)
2959 self
.treeviewChannelChooser
.set_sensitive(True)
2962 def thread_func(self
):
2963 url
= self
.entryURL
.get_text()
2964 importer
= opml
.Importer(url
)
2965 model
= importer
.get_model()
2967 self
.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
2968 util
.idle_add(self
.thread_finished
, model
)
2970 def get_channels_from_url( self
, url
, callback_for_channel
= None, callback_finished
= None):
2971 if callback_for_channel
:
2972 self
.callback_for_channel
= callback_for_channel
2973 if callback_finished
:
2974 self
.callback_finished
= callback_finished
2975 self
.labelStatus
.set_label( _('Downloading, please wait...'))
2976 self
.entryURL
.set_text( url
)
2977 self
.btnDownloadOpml
.set_sensitive( False)
2978 self
.entryURL
.set_sensitive( False)
2979 self
.btnOK
.set_sensitive( False)
2980 self
.treeviewChannelChooser
.set_sensitive( False)
2981 Thread( target
= self
.thread_func
).start()
2983 def select_all( self
, value
):
2985 for row
in self
.treeviewChannelChooser
.get_model():
2988 self
.channels
.append(row
[2])
2989 self
.btnOK
.set_sensitive(bool(len(self
.channels
)))
2991 def on_gPodderOpmlLister_destroy(self
, widget
, *args
):
2994 def on_btnDownloadOpml_clicked(self
, widget
, *args
):
2995 self
.get_channels_from_url( self
.entryURL
.get_text())
2997 def on_btnSelectAll_clicked(self
, widget
, *args
):
2998 self
.select_all(True)
3000 def on_btnSelectNone_clicked(self
, widget
, *args
):
3001 self
.select_all(False)
3003 def on_btnOK_clicked(self
, widget
, *args
):
3004 self
.gPodderOpmlLister
.destroy()
3006 # add channels that have been selected
3007 for url
in self
.channels
:
3008 if self
.callback_for_channel
:
3009 self
.callback_for_channel( url
)
3011 if self
.callback_finished
:
3012 self
.callback_finished()
3014 def on_btnCancel_clicked(self
, widget
, *args
):
3015 self
.gPodderOpmlLister
.destroy()
3018 class gPodderEpisodeSelector( GladeWidget
):
3019 """Episode selection dialog
3021 Optional keyword arguments that modify the behaviour of this dialog:
3023 - callback: Function that takes 1 parameter which is a list of
3024 the selected episodes (or empty list when none selected)
3025 - episodes: List of episodes that are presented for selection
3026 - selected: (optional) List of boolean variables that define the
3027 default checked state for the given episodes
3028 - selected_default: (optional) The default boolean value for the
3029 checked state if no other value is set
3031 - columns: List of (name,caption) pairs for the columns, the name
3032 is the attribute name of the episode to be read from
3033 each episode object and the caption attribute is the
3034 text that appear as column caption
3035 (default is [('title','Episode'),])
3036 - title: (optional) The title of the window + heading
3037 - instructions: (optional) A one-line text describing what the
3038 user should select / what the selection is for
3039 - stock_ok_button: (optional) Will replace the "OK" button with
3040 another GTK+ stock item to be used for the
3041 affirmative button of the dialog (e.g. can
3042 be gtk.STOCK_DELETE when the episodes to be
3043 selected will be deleted after closing the
3045 - selection_buttons: (optional) A dictionary with labels as
3046 keys and callbacks as values; for each
3047 key a button will be generated, and when
3048 the button is clicked, the callback will
3049 be called for each episode and the return
3050 value of the callback (True or False) will
3051 be the new selected state of the episode
3052 - size_attribute: (optional) The name of an attribute of the
3053 supplied episode objects that can be used to
3054 calculate the size of an episode; set this to
3055 None if no total size calculation should be
3056 done (in cases where total size is useless)
3057 (default is 'length')
3060 finger_friendly_widgets
= ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3063 COLUMN_ADDITIONAL
= 1
3066 if not hasattr( self
, 'callback'):
3067 self
.callback
= None
3069 if not hasattr( self
, 'episodes'):
3072 if not hasattr( self
, 'size_attribute'):
3073 self
.size_attribute
= 'length'
3075 if not hasattr( self
, 'selection_buttons'):
3076 self
.selection_buttons
= {}
3078 if not hasattr( self
, 'selected_default'):
3079 self
.selected_default
= False
3081 if not hasattr( self
, 'selected'):
3082 self
.selected
= [self
.selected_default
]*len(self
.episodes
)
3084 if len(self
.selected
) < len(self
.episodes
):
3085 self
.selected
+= [self
.selected_default
]*(len(self
.episodes
)-len(self
.selected
))
3087 if not hasattr( self
, 'columns'):
3088 self
.columns
= ( ('title', _('Episode')), )
3090 if hasattr( self
, 'title'):
3091 self
.gPodderEpisodeSelector
.set_title( self
.title
)
3092 self
.labelHeading
.set_markup( '<b><big>%s</big></b>' % saxutils
.escape( self
.title
))
3094 if gpodder
.interface
== gpodder
.MAEMO
:
3095 self
.labelHeading
.hide()
3097 if hasattr( self
, 'instructions'):
3098 self
.labelInstructions
.set_text( self
.instructions
)
3099 self
.labelInstructions
.show_all()
3101 if hasattr(self
, 'stock_ok_button'):
3102 if self
.stock_ok_button
== 'gpodder-download':
3103 self
.btnOK
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_BUTTON
))
3104 self
.btnOK
.set_label(_('Download'))
3106 self
.btnOK
.set_label(self
.stock_ok_button
)
3107 self
.btnOK
.set_use_stock(True)
3109 toggle_cell
= gtk
.CellRendererToggle()
3110 toggle_cell
.connect( 'toggled', self
.toggle_cell_handler
)
3112 self
.treeviewEpisodes
.append_column( gtk
.TreeViewColumn( '', toggle_cell
, active
=self
.COLUMN_TOGGLE
))
3114 next_column
= self
.COLUMN_ADDITIONAL
3115 for name
, caption
in self
.columns
:
3116 renderer
= gtk
.CellRendererText()
3117 renderer
.set_property( 'ellipsize', pango
.ELLIPSIZE_END
)
3118 column
= gtk
.TreeViewColumn( caption
, renderer
, text
=next_column
)
3119 column
.set_resizable( True)
3120 # Only set "expand" on the first column (so more text is displayed there)
3121 column
.set_expand(next_column
== self
.COLUMN_ADDITIONAL
)
3122 self
.treeviewEpisodes
.append_column( column
)
3125 column_types
= [ gobject
.TYPE_BOOLEAN
] + [ gobject
.TYPE_STRING
] * len(self
.columns
)
3126 self
.model
= gtk
.ListStore( *column_types
)
3128 for index
, episode
in enumerate( self
.episodes
):
3129 row
= [ self
.selected
[index
] ]
3130 for name
, caption
in self
.columns
:
3131 if not hasattr(episode
, name
):
3132 log('Warning: Missing attribute "%s"', name
, sender
=self
)
3135 row
.append(getattr( episode
, name
))
3136 self
.model
.append( row
)
3138 for label
in self
.selection_buttons
:
3139 button
= gtk
.Button( label
)
3140 button
.connect('clicked', self
.custom_selection_button_clicked
, label
)
3141 self
.hboxButtons
.pack_start( button
, expand
= False)
3144 self
.treeviewEpisodes
.set_rules_hint( True)
3145 self
.treeviewEpisodes
.set_model( self
.model
)
3146 self
.treeviewEpisodes
.columns_autosize()
3147 self
.calculate_total_size()
3149 def calculate_total_size( self
):
3150 if self
.size_attribute
is not None:
3151 (total_size
, count
) = (0, 0)
3152 for index
, row
in enumerate( self
.model
):
3153 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
3155 total_size
+= int(getattr( self
.episodes
[index
], self
.size_attribute
))
3158 log( 'Cannot get size for %s', self
.episodes
[index
].title
, sender
= self
)
3163 text
.append(_('One episodes selected'))
3165 text
.append(_('%d episodes selected') % count
)
3166 text
.append(_('total size: %s') % gl
.format_filesize(total_size
))
3167 self
.labelTotalSize
.set_text(', '.join(text
))
3168 self
.btnOK
.set_sensitive(True)
3170 self
.labelTotalSize
.set_text(_('Nothing selected'))
3171 self
.btnOK
.set_sensitive(False)
3173 self
.btnOK
.set_sensitive(False)
3174 for index
, row
in enumerate(self
.model
):
3175 if self
.model
.get_value(row
.iter, self
.COLUMN_TOGGLE
) == True:
3176 self
.btnOK
.set_sensitive(True)
3178 self
.labelTotalSize
.set_text('')
3180 def toggle_cell_handler( self
, cell
, path
):
3181 model
= self
.treeviewEpisodes
.get_model()
3182 model
[path
][self
.COLUMN_TOGGLE
] = not model
[path
][self
.COLUMN_TOGGLE
]
3184 self
.calculate_total_size()
3186 def custom_selection_button_clicked(self
, button
, label
):
3187 callback
= self
.selection_buttons
[label
]
3189 for index
, row
in enumerate( self
.model
):
3190 new_value
= callback( self
.episodes
[index
])
3191 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, new_value
)
3193 self
.calculate_total_size()
3195 def on_btnCheckAll_clicked( self
, widget
):
3196 for row
in self
.model
:
3197 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, True)
3199 self
.calculate_total_size()
3201 def on_btnCheckNone_clicked( self
, widget
):
3202 for row
in self
.model
:
3203 self
.model
.set_value( row
.iter, self
.COLUMN_TOGGLE
, False)
3205 self
.calculate_total_size()
3207 def get_selected_episodes( self
):
3208 selected_episodes
= []
3210 for index
, row
in enumerate( self
.model
):
3211 if self
.model
.get_value( row
.iter, self
.COLUMN_TOGGLE
) == True:
3212 selected_episodes
.append( self
.episodes
[index
])
3214 return selected_episodes
3216 def on_btnOK_clicked( self
, widget
):
3217 self
.gPodderEpisodeSelector
.destroy()
3218 if self
.callback
is not None:
3219 self
.callback( self
.get_selected_episodes())
3221 def on_btnCancel_clicked( self
, widget
):
3222 self
.gPodderEpisodeSelector
.destroy()
3223 if self
.callback
is not None:
3226 class gPodderConfigEditor(GladeWidget
):
3227 finger_friendly_widgets
= ['btnShowAll', 'btnClose', 'configeditor']
3230 name_column
= gtk
.TreeViewColumn(_('Variable'))
3231 name_renderer
= gtk
.CellRendererText()
3232 name_column
.pack_start(name_renderer
)
3233 name_column
.add_attribute(name_renderer
, 'text', 0)
3234 name_column
.add_attribute(name_renderer
, 'weight', 5)
3235 self
.configeditor
.append_column(name_column
)
3237 type_column
= gtk
.TreeViewColumn(_('Type'))
3238 type_renderer
= gtk
.CellRendererText()
3239 type_column
.pack_start(type_renderer
)
3240 type_column
.add_attribute(type_renderer
, 'text', 1)
3241 type_column
.add_attribute(type_renderer
, 'weight', 5)
3242 self
.configeditor
.append_column(type_column
)
3244 value_column
= gtk
.TreeViewColumn(_('Value'))
3245 value_renderer
= gtk
.CellRendererText()
3246 value_column
.pack_start(value_renderer
)
3247 value_column
.add_attribute(value_renderer
, 'text', 2)
3248 value_column
.add_attribute(value_renderer
, 'editable', 4)
3249 value_column
.add_attribute(value_renderer
, 'weight', 5)
3250 value_renderer
.connect('edited', self
.value_edited
)
3251 self
.configeditor
.append_column(value_column
)
3253 self
.model
= gl
.config
.model()
3254 self
.filter = self
.model
.filter_new()
3255 self
.filter.set_visible_func(self
.visible_func
)
3257 self
.configeditor
.set_model(self
.filter)
3258 self
.configeditor
.set_rules_hint(True)
3260 def visible_func(self
, model
, iter, user_data
=None):
3261 text
= self
.entryFilter
.get_text().lower()
3265 # either the variable name or its value
3266 return (text
in model
.get_value(iter, 0).lower() or
3267 text
in model
.get_value(iter, 2).lower())
3269 def value_edited(self
, renderer
, path
, new_text
):
3270 model
= self
.configeditor
.get_model()
3271 iter = model
.get_iter(path
)
3272 name
= model
.get_value(iter, 0)
3273 type_cute
= model
.get_value(iter, 1)
3275 if not gl
.config
.update_field(name
, new_text
):
3276 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
))
3278 def on_entryFilter_changed(self
, widget
):
3279 self
.filter.refilter()
3281 def on_btnShowAll_clicked(self
, widget
):
3282 self
.entryFilter
.set_text('')
3283 self
.entryFilter
.grab_focus()
3285 def on_configeditor_row_activated(self
, treeview
, path
, view_column
):
3286 model
= treeview
.get_model()
3287 it
= model
.get_iter(path
)
3288 field_name
= model
.get_value(it
, 0)
3289 field_type
= model
.get_value(it
, 3)
3291 # Flip the boolean config flag
3292 if field_type
== bool:
3293 gl
.config
.toggle_flag(field_name
)
3295 def on_btnClose_clicked(self
, widget
):
3296 self
.gPodderConfigEditor
.destroy()
3300 gobject
.threads_init()
3301 gtk
.window_set_default_icon_name( 'gpodder')