Tue, 08 Jul 2008 21:10:48 -0400 <me@nikosapi.org>
[gpodder.git] / src / gpodder / gui.py
blob29e63111c49f6d655173103c072c2d77dfcf2dd8
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/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib2
31 import datetime
33 from xml.sax import saxutils
35 from threading import Event
36 from threading import Thread
37 from string import strip
39 import gpodder
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
49 try:
50 from gpodder import trayicon
51 have_trayicon = True
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!')
55 have_trayicon = False
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:
75 import hildon
76 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
78 app_name = "gpodder"
79 app_version = "unknown" # will be set in main() call
80 app_authors = [
81 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
82 '',
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__
126 domain = app_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
136 else:
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)
148 else:
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:
156 if title is None:
157 title = 'gPodder'
158 self.tray_icon.send_notification(message, title)
159 return
161 if gpodder.interface == gpodder.GUI:
162 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
163 if title:
164 dlg.set_title(str(title))
165 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
166 else:
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))
171 dlg.run()
172 dlg.destroy()
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
180 scrollbar, etc..)
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)
189 else:
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)
198 else:
199 log('Cannot set widget finger-friendly: %s', widget, sender=self)
201 return widget
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)
207 if title:
208 dlg.set_title(str(title))
209 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
210 else:
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))
216 response = dlg.run()
217 dlg.destroy()
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)
251 try:
252 shutil.copyfile( src_filename, dst_filename)
253 except:
254 log( 'Error copying file.', sender = self, traceback = True)
256 dlg.destroy()
260 class gPodder(GladeWidget):
261 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
262 ENTER_URL_TEXT = _('Enter podcast URL...')
264 def new(self):
265 if gpodder.interface == gpodder.MAEMO:
266 # Maemo-specific changes to the UI
267 global scalable_dir
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
287 menu = gtk.Menu()
288 for child in self.mainMenu.get_children():
289 child.reparent(menu)
290 self.itemQuit.reparent(menu)
291 self.window.set_menu(menu)
293 self.mainMenu.destroy()
294 self.window.show()
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)
322 self.uar = 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)
353 else:
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
386 try:
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)
391 except:
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)
401 else:
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 = ''
408 else:
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:
462 self.gPodder.show()
464 if self.tray_icon:
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))
491 else:
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:
524 return
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:
536 widget.set_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':
545 if new_value:
546 self.toolbar.show_all()
547 else:
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:
554 if new_value:
555 self.hboxAddChannel.show_all()
556 else:
557 self.hboxAddChannel.hide_all()
559 def read_apps(self):
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()
571 y -= x_bin
572 y -= y_bin
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
577 return False
579 if path is not 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
586 return False
587 self.last_tooltip_episode = url
589 tooltip.set_text(description)
590 return True
592 self.last_tooltip_episode = None
593 return False
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
606 return False
608 if path is not 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
616 return False
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)
621 if error_str:
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
643 else:
644 pos = channel.description.find('\n\n')
645 if pos == -1 or pos > 500:
646 description = channel.description[:498]+'[...]'
647 else:
648 description = channel.description[:pos]
650 description = gtk.Label(description)
651 if error_str:
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)
657 table.show_all()
658 tooltip.set_custom(table)
660 return True
662 self.last_tooltip_channel = None
663 return False
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
676 paths = []
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
685 # on the selection
686 paths = []
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
698 if not len( paths):
699 return True
701 menu = gtk.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))
706 menu.append( item)
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)
712 menu.append(item)
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))
718 menu.append(item)
720 menu.append( gtk.SeparatorMenuItem())
722 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
723 item.connect( 'activate', self.on_itemEditChannel_activate)
724 menu.append( item)
726 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
727 item.connect( 'activate', self.on_itemRemoveChannel_activate)
728 menu.append( item)
730 menu.show_all()
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)
737 return True
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)
743 else:
744 self.iconify_main_window()
745 else:
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:
787 device = None
788 else:
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):
794 destfile += 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)
800 dlg.set_title(title)
801 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
802 dlg.show_all()
803 else:
804 dlg = None
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)
809 result = p.wait()
810 if dialog is not None:
811 dialog.destroy()
812 else:
813 try:
814 shutil.copyfile(filename, destfile)
815 result = 0
816 except:
817 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
818 result = 1
820 if result == 0 or not os.path.exists(destfile):
821 util.bluetooth_send_file(destfile, device)
822 else:
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
837 paths = []
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
846 # on the selection
847 paths = []
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
859 if not len( paths):
860 return True
862 first_url = model.get_value( model.get_iter( paths[0]), 0)
863 episode = db.load_episode(first_url)
865 menu = gtk.Menu()
867 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = self.play_or_download()
869 if can_play:
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))
879 if can_cancel:
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))
885 if can_download:
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))
902 if can_play:
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))
913 if can_transfer:
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))
919 if can_play:
920 menu.append( gtk.SeparatorMenuItem())
921 is_played = episode['is_played']
922 if 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))
927 else:
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']
934 if 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))
939 else:
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))
945 if len(paths) == 1:
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))
969 menu.show_all()
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)
976 return True
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 ]
985 if count == 1:
986 title.append( _('downloading one file'))
987 elif count > 1:
988 title.append( _('downloading %d files') % count)
990 if len(title) == 2:
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
997 if count == 0:
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)
1003 if not success:
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):
1008 if model is None:
1009 return True
1011 key = key.lower()
1013 # columns, as defined in libpodcasts' get model method
1014 # 1 = episode title, 7 = description
1015 columns = (1, 7)
1017 for column in columns:
1018 value = model.get_value( iter, column).lower()
1019 if value.find( key) != -1:
1020 return False
1022 return True
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:
1033 return
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()
1042 for path in paths:
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):
1049 can_play = True
1050 can_delete = True
1051 is_played = episode.is_played
1052 is_locked = episode.is_locked
1053 else:
1054 if services.download_status_manager.is_download_in_progress(url):
1055 can_cancel = True
1056 else:
1057 can_download = True
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)
1071 if can_cancel:
1072 self.item_cancel_download.show_all()
1073 else:
1074 self.item_cancel_download.hide_all()
1075 if can_download:
1076 self.itemDownloadSelected.show_all()
1077 else:
1078 self.itemDownloadSelected.hide_all()
1079 if can_play:
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()
1085 if is_played:
1086 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1087 else:
1088 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1089 if is_locked:
1090 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1091 else:
1092 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1093 else:
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()
1103 else:
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()
1112 if count:
1113 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1114 else:
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)
1142 try:
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)
1153 break
1154 pos = model.iter_next(pos)
1156 self.treeChannels.get_selection().select_path(selected_path)
1157 except:
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()
1166 else:
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
1173 dnd_channel = None
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
1181 break
1183 result = sel.data
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)
1187 else:
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)
1193 if 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)
1202 break
1203 self.show_message( _('You have already subscribed to this podcast: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
1204 return
1205 log( 'Adding new channel: %s', result)
1206 try:
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)
1210 channel = None
1212 if channel is not None:
1213 self.channels.append( channel)
1214 save_channels( self.channels)
1215 if not quiet:
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)
1224 channel.save()
1226 if ask_download_new:
1227 new_episodes = channel.get_new_episodes()
1228 if len(new_episodes):
1229 self.new_episodes_show(new_episodes)
1230 else:
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...
1233 try:
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)
1238 return
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)
1245 return
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)
1252 else:
1253 if result:
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)
1257 else:
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
1263 if force_update:
1264 progression = _('Updating %s (%d/%d)')%(title, position+1, count)
1265 else:
1266 progression = _('Loading %s (%d/%d)')%(title, position+1, count)
1267 progressbar.set_text(progression)
1268 if self.tray_icon:
1269 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
1271 if count > 0:
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)
1283 if self.tray_icon:
1284 self.tray_icon.set_status(None)
1285 if self.minimized and force_update:
1286 new_episodes = []
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)
1299 return
1300 elif len(new_episodes) == 1:
1301 title = _('gPodder has found %s') % (_('one new episode:'),)
1302 else:
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)
1311 return
1313 # open the episodes selection dialog
1314 if force_update:
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()
1320 else:
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...'))
1325 if finish_proc:
1326 finish_proc()
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):
1333 if self.tray_icon:
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)
1347 thread.start()
1349 def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
1350 if self.active_channel is None:
1351 return
1353 current_channel = self.active_channel
1354 current_podcast = current_channel.find_episode( url)
1355 filename = current_podcast.local_filename()
1357 if widget:
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)
1361 # open the file now
1362 if current_podcast.file_type() != 'torrent':
1363 self.playback_episode( current_channel, current_podcast)
1364 return
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)
1370 return
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()
1374 else:
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?'))
1403 if result:
1404 self.close_gpodder()
1405 else:
1406 return True
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')
1412 if downloading:
1413 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1414 else:
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))
1419 if not downloading:
1420 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1421 dialog.vbox.pack_start(cb_ask)
1422 cb_ask.show_all()
1424 result = dialog.run()
1425 dialog.destroy()
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()
1431 else:
1432 self.close_gpodder()
1434 return True
1436 def close_gpodder(self):
1437 """ clean everything and exit properly
1439 if self.channels:
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()
1446 sys.exit( 0)
1448 def get_old_episodes(self):
1449 episodes = []
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)
1454 return episodes
1456 def for_each_selected_episode_url( self, callback):
1457 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1458 for path in paths:
1459 url = model.get_value( model.get_iter( path), 0)
1460 try:
1461 callback( url)
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:
1469 return
1471 if len(episodes) == 1:
1472 message = _('Do you really want to delete this episode?')
1473 else:
1474 message = _('Do you really want to delete %d episodes?') % len(episodes)
1476 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1477 return
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):
1486 columns = (
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.')
1502 episodes = []
1503 selected = []
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):
1524 if toggle:
1525 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1526 else:
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):
1532 if toggle:
1533 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1534 else:
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()
1551 if self.channels:
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()
1563 l = gtk.Label()
1564 l.set_padding(6, 3)
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)
1568 l = gtk.Label()
1569 l.set_padding(6, 3)
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)
1574 lb = gtk.Label()
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)
1588 w.show_all()
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)
1598 w.run()
1599 w.destroy()
1600 else:
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):
1616 columns = (
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)
1630 else:
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):
1636 episodes = []
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"""
1645 episode_list = []
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)
1649 continue
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)
1653 return episode_list
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)
1662 else:
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)
1668 break
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 )
1676 else:
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()
1682 if device is None:
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)
1686 return
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)
1692 return
1694 gPodderSync(device=device, gPodder=self)
1695 if self.tray_icon:
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)
1705 else:
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)
1712 return
1714 if self.tray_icon:
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)
1732 return
1734 def on_cleanup_ipod_activate(self, widget, *args):
1735 columns = (
1736 ('title', _('Episode')),
1737 ('podcast', _('Podcast')),
1738 ('filesize', _('Size')),
1739 ('modified', _('Copied')),
1740 ('playcount', _('Play count')),
1741 ('released', _('Released')),
1744 device = sync.open_device()
1746 if device is None:
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)
1750 return
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)
1756 return
1758 gPodderSync(device=device, gPodder=self)
1760 tracks = device.get_all_tracks()
1761 if len(tracks) > 0:
1762 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1763 wanted_columns = []
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
1769 break
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)
1777 else:
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)
1787 del self.tray_icon
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())
1806 else:
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)
1817 else:
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)
1823 else:
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)
1833 return
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):
1838 channel = None
1839 try:
1840 channel = podcastChannel.load(url=new_url, create=True)
1841 except:
1842 channel = None
1844 if channel is None:
1845 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1846 return
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)
1858 for file in files:
1859 log('moving %s', file, sender=self)
1860 shutil.move(file, new_save_dir)
1861 try:
1862 os.rmdir(old_save_dir)
1863 except:
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):
1871 try:
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)
1885 cb_ask.show_all()
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()
1893 dialog.destroy()
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()
1899 else:
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
1912 else:
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)
1924 except:
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)')
1932 return filter
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()
1944 filename = None
1945 if response == gtk.RESPONSE_OK:
1946 filename = dlg.get_filename()
1947 dlg.destroy()
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)
1957 return
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'))
1973 dlg.destroy()
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)
2003 try:
2004 dlg.set_logo(gtk.gdk.pixbuf_new_from_file_at_size(scalable_dir, 200, 200))
2005 except:
2006 pass
2008 dlg.run()
2010 def on_wNotebook_switch_page(self, widget, *args):
2011 page_num = args[1]
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)
2017 else:
2018 self.set_title(tab_label)
2019 if page_num == 0:
2020 self.play_or_download()
2021 else:
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()
2041 else:
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):
2061 try:
2062 selection = self.treeAvailable.get_selection()
2063 selection_tuple = selection.get_selected_rows()
2064 transfer_files = False
2065 episodes = []
2067 if selection.count_selected_rows() > 1:
2068 widget_to_send = None
2069 show_message_dialog = False
2070 else:
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)
2082 if transfer_files:
2083 episodes.append( self.active_channel.find_episode( url))
2084 else:
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)
2090 except:
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):
2110 cancel_urls = []
2112 if self.wNotebook.get_current_page() > 0:
2113 # Use the download list treeview + model
2114 ( tree, column ) = ( self.treeDownloads, 3 )
2115 else:
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()
2121 for path in paths:
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)
2127 return
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.")
2132 else:
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:
2155 return
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.')
2163 return
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)
2173 return
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.")
2177 else:
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.')
2181 locked_count = 0
2182 for path in paths:
2183 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2184 if episode['is_locked']:
2185 locked_count += 1
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)
2191 return
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):
2198 try:
2199 # iterate over the selection, see also on_treeDownloads_row_activated
2200 for path in paths:
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()
2206 except:
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:
2219 return
2221 if event.keyval == gtk.keysyms.F6:
2222 if self.fullscreen:
2223 self.window.unfullscreen()
2224 else:
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)
2232 diff = 0
2233 if event.keyval == gtk.keysyms.F7: #plus
2234 diff = 1
2235 elif event.keyval == gtk.keysyms.F8: #minus
2236 diff = -1
2238 if diff != 0:
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
2247 else:
2248 self.fullscreen = False
2250 old_minimized = self.minimized
2252 if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
2253 self.minimized = True
2254 else:
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):
2266 if self.minimized:
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']
2276 def new(self):
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:
2324 url = dlg.get_uri()
2325 services.cover_downloader.replace_cover(self.channel, url)
2327 dlg.destroy()
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')
2339 if len(files) != 1:
2340 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2341 return
2343 file = files[0]
2345 if file.startswith('file://') or file.startswith('http://'):
2346 services.cover_downloader.replace_cover(self.channel, file)
2347 return
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()
2369 self.channel.save()
2371 self.gPodderChannel.destroy()
2372 self.callback_closed()
2374 class gPodderAddPodcastDialog(GladeWidget):
2375 finger_friendly_widgets = ['btn_close', 'btn_add']
2377 def new(self):
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']
2398 def new(self):
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):
2422 def new(self):
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)
2481 else:
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)
2486 # device type
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( '')
2526 else:
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
2546 found = False
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)
2552 found = True
2553 break
2554 if not found:
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()
2562 index = 0
2563 while iter is not None:
2564 command = model.get_value(iter, 1)
2565 if command == self.openApp.get_text():
2566 return index
2567 iter = model.iter_next(iter)
2568 index += 1
2569 # return last item = custom command
2570 return index-1
2572 def find_active_video_app( self):
2573 model = self.comboVideoPlayerApp.get_model()
2574 iter = model.get_iter_first()
2575 index = 0
2576 while iter is not None:
2577 command = model.get_value(iter, 1)
2578 if command == self.openVideoApp.get_text():
2579 return index
2580 iter = model.iter_next(iter)
2581 index += 1
2582 # return last item = custom command
2583 return index-1
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'))
2590 if event:
2591 event.set()
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):
2615 examples = [
2616 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2617 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2618 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
2621 info = [
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)
2641 if command == '':
2642 self.openApp.set_sensitive( True)
2643 self.openApp.show()
2644 self.labelCustomCommand.show()
2645 else:
2646 self.openApp.set_text( command)
2647 self.openApp.set_sensitive( False)
2648 self.openApp.hide()
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)
2656 if command == '':
2657 self.openVideoApp.set_sensitive(True)
2658 self.openVideoApp.show()
2659 self.label115.show()
2660 else:
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()
2674 # None
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:
2681 widget.hide_all()
2682 else:
2683 widget.show_all()
2685 # iPod
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:
2690 widget.show_all()
2691 else:
2692 widget.hide_all()
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:
2700 widget.show_all()
2701 else:
2702 widget.hide_all()
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())
2711 fs.destroy()
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())
2720 fs.destroy()
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)
2730 event = Event()
2732 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
2733 dlg.vbox.set_spacing( 5)
2734 dlg.set_border_width( 5)
2736 label = gtk.Label()
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)
2745 # switch windows
2746 dlg.show_all()
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)
2756 thread.start()
2758 while not event.isSet():
2759 try:
2760 new_download_dir_size = util.calculate_size( new_download_dir)
2761 except:
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)
2765 else:
2766 fract = 0.0
2767 if fract < 0.99:
2768 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
2769 else:
2770 myprogressbar.set_text( _('Finishing... please wait.'))
2771 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
2772 event.wait( 0.1)
2773 while gtk.events_pending():
2774 gtk.main_iteration( False)
2776 dlg.destroy()
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']
2794 def new(self):
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)
2845 if is_downloading:
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()
2851 else:
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()
2858 else:
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):
2881 def new(self):
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))
2899 def on_done(self):
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']
2918 def new(self):
2919 # initiate channels list
2920 self.channels = []
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]
2947 if model[path][0]:
2948 self.channels.append( url)
2949 else:
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)
2960 self.channels = []
2962 def thread_func(self):
2963 url = self.entryURL.get_text()
2964 importer = opml.Importer(url)
2965 model = importer.get_model()
2966 if len(model) == 0:
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 ):
2984 self.channels = []
2985 for row in self.treeviewChannelChooser.get_model():
2986 row[0] = value
2987 if value:
2988 self.channels.append(row[2])
2989 self.btnOK.set_sensitive(bool(len(self.channels)))
2991 def on_gPodderOpmlLister_destroy(self, widget, *args):
2992 pass
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
3030 (default is False)
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
3044 dialog)
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']
3062 COLUMN_TOGGLE = 0
3063 COLUMN_ADDITIONAL = 1
3065 def new( self):
3066 if not hasattr( self, 'callback'):
3067 self.callback = None
3069 if not hasattr( self, 'episodes'):
3070 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'))
3105 else:
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)
3123 next_column += 1
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)
3133 row.append(None)
3134 else:
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)
3142 button.show_all()
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:
3154 try:
3155 total_size += int(getattr( self.episodes[index], self.size_attribute))
3156 count += 1
3157 except:
3158 log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
3160 if total_size > 0:
3161 text = []
3162 if count == 1:
3163 text.append(_('One episodes selected'))
3164 else:
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)
3169 else:
3170 self.labelTotalSize.set_text(_('Nothing selected'))
3171 self.btnOK.set_sensitive(False)
3172 else:
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)
3177 break
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:
3224 self.callback([])
3226 class gPodderConfigEditor(GladeWidget):
3227 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3229 def new(self):
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()
3262 if text == '':
3263 return True
3264 else:
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()
3299 def main():
3300 gobject.threads_init()
3301 gtk.window_set_default_icon_name( 'gpodder')
3303 gPodder().run()