Fix delete/download actions for missing episodes
[gpodder.git] / src / gpodder / gui.py
blobb7aa6de7bce013cd913937daf4c019467fdbceb6
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 urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import util
43 from gpodder import opml
44 from gpodder import services
45 from gpodder import sync
46 from gpodder import download
47 from gpodder import SimpleGladeApp
48 from gpodder.liblogger import log
49 from gpodder.dbsqlite import db
51 try:
52 from gpodder import trayicon
53 have_trayicon = True
54 except Exception, exc:
55 log('Warning: Could not import gpodder.trayicon.', traceback=True)
56 log('Warning: This probably means your PyGTK installation is too old!')
57 have_trayicon = False
59 from libpodcasts import podcastChannel
60 from libpodcasts import LocalDBReader
61 from libpodcasts import podcastItem
62 from libpodcasts import channels_to_model
63 from libpodcasts import update_channel_model_by_iter
64 from libpodcasts import load_channels
65 from libpodcasts import update_channels
66 from libpodcasts import save_channels
67 from libpodcasts import can_restore_from_opml
68 from libpodcasts import HTTPAuthError
70 from gpodder.libgpodder import gl
72 from libplayers import UserAppsReader
74 from libtagupdate import tagging_supported
76 if gpodder.interface == gpodder.GUI:
77 WEB_BROWSER_ICON = 'web-browser'
78 elif gpodder.interface == gpodder.MAEMO:
79 import hildon
80 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
82 app_name = "gpodder"
83 app_version = "unknown" # will be set in main() call
84 app_authors = [
85 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
86 '',
87 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
88 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andy Busch',
89 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
90 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
91 'Carlos Moffat', 'Chris', 'Chris Arnold', 'Clark Burbidge', 'Daniel Ramos',
92 'David Spreen', 'Doug Hellmann', 'FFranci72', 'Florian Richter', 'Frank Harper',
93 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Götz Waschk',
94 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
95 'Jérôme Chabod', 'Jerry Moss',
96 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
97 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
98 'Jürgen Schinker', 'Justin Forest',
99 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Michael Salim',
100 'Mika Leppinen', 'Mike Coulson', 'Mykola Nikishov', 'narf at inode.at',
101 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
102 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
103 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
104 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
105 'Robert Young', 'Roel Groeneveld',
106 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
107 'Stefan Lohmaier', 'Stephan Buys', 'Stylianos Papanastasiou', 'Teo Ramirez',
108 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
109 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'VladDrac',
110 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
112 'List may be incomplete - please contact me.'
114 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
115 app_website = 'http://www.gpodder.org/'
117 # these will be filled with pathnames in bin/gpodder
118 glade_dir = [ 'share', 'gpodder' ]
119 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
120 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
123 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
124 gpodder_main_window = None
125 finger_friendly_widgets = []
127 def __init__( self, **kwargs):
128 path = os.path.join( glade_dir, '%s.glade' % app_name)
129 root = self.__class__.__name__
130 domain = app_name
132 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
134 # Set widgets to finger-friendly mode if on Maemo
135 for widget_name in self.finger_friendly_widgets:
136 self.set_finger_friendly(getattr(self, widget_name))
138 if root == 'gPodder':
139 GladeWidget.gpodder_main_window = self.gPodder
140 else:
141 # If we have a child window, set it transient for our main window
142 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
144 if gpodder.interface == gpodder.GUI:
145 if hasattr( self, 'center_on_widget'):
146 ( x, y ) = self.gpodder_main_window.get_position()
147 a = self.center_on_widget.allocation
148 ( x, y ) = ( x + a.x, y + a.y )
149 ( w, h ) = ( a.width, a.height )
150 ( pw, ph ) = getattr( self, root).get_size()
151 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
152 else:
153 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
155 def notification(self, message, title=None):
156 util.idle_add(self.show_message, message, title)
158 def show_message( self, message, title = None):
159 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
160 if title is None:
161 title = 'gPodder'
162 self.tray_icon.send_notification(message, title)
163 return
165 if gpodder.interface == gpodder.GUI:
166 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
167 if title:
168 dlg.set_title(str(title))
169 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
170 else:
171 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
172 elif gpodder.interface == gpodder.MAEMO:
173 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
175 dlg.run()
176 dlg.destroy()
178 def set_finger_friendly(self, widget):
180 If we are on Maemo, we carry out the necessary
181 operations to turn a widget into a finger-friendly
182 one, depending on which type of widget it is (i.e.
183 buttons will have more padding, TreeViews a thick
184 scrollbar, etc..)
186 if gpodder.interface == gpodder.MAEMO:
187 if isinstance(widget, gtk.Misc):
188 widget.set_padding(0, 5)
189 elif isinstance(widget, gtk.Button):
190 for child in widget.get_children():
191 if isinstance(child, gtk.Alignment):
192 child.set_padding(10, 10, 5, 5)
193 else:
194 child.set_padding(10, 10)
195 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
196 parent = widget.get_parent()
197 if isinstance(parent, gtk.ScrolledWindow):
198 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
199 elif isinstance(widget, gtk.MenuItem):
200 for child in widget.get_children():
201 self.set_finger_friendly(child)
202 else:
203 log('Cannot set widget finger-friendly: %s', widget, sender=self)
205 return widget
207 def show_confirmation( self, message, title = None):
208 if gpodder.interface == gpodder.GUI:
209 affirmative = gtk.RESPONSE_YES
210 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
211 if title:
212 dlg.set_title(str(title))
213 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
214 else:
215 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
216 elif gpodder.interface == gpodder.MAEMO:
217 affirmative = gtk.RESPONSE_OK
218 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
220 response = dlg.run()
221 dlg.destroy()
223 return response == affirmative
225 def UsernamePasswordDialog( self, title, message ):
226 """ An authentication dialog based on
227 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
229 dialog = gtk.MessageDialog(
230 GladeWidget.gpodder_main_window,
231 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
232 gtk.MESSAGE_QUESTION,
233 gtk.BUTTONS_OK_CANCEL )
235 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
236 dialog.set_title(title)
237 dialog.format_secondary_markup(message)
239 username_entry = gtk.Entry()
240 username_entry.set_width_chars(25)
241 password_entry = gtk.Entry()
242 password_entry.set_width_chars(25)
243 password_entry.set_visibility(False)
245 username_hbox = gtk.HBox()
246 username_label = gtk.Label()
247 username_label.set_markup('<b>' + _('Username:') + '</b>')
248 username_hbox.pack_start(username_label, False, 5, 5)
249 username_hbox.pack_end(username_entry, False)
251 password_hbox = gtk.HBox()
252 password_label = gtk.Label()
253 password_label.set_markup('<b>' + _('Password:') + '</b>')
254 password_hbox.pack_start(password_label, False, 5, 5)
255 password_hbox.pack_end(password_entry, False)
257 vbox = gtk.VBox(spacing=5)
258 vbox.pack_start(username_hbox)
259 vbox.pack_start(password_hbox)
261 dialog.vbox.pack_end(vbox, True, True, 0)
262 dialog.show_all()
263 response = dialog.run()
265 password_entry.set_visibility(True)
266 dialog.destroy()
268 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
270 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
271 if dst_filename is None:
272 dst_filename = src_filename
274 if dst_directory is None:
275 dst_directory = os.path.expanduser( '~')
277 ( base, extension ) = os.path.splitext( src_filename)
279 if not dst_filename.endswith( extension):
280 dst_filename += extension
282 if gpodder.interface == gpodder.GUI:
283 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
284 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
285 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
286 elif gpodder.interface == gpodder.MAEMO:
287 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
289 dlg.set_do_overwrite_confirmation( True)
290 dlg.set_current_name( os.path.basename( dst_filename))
291 dlg.set_current_folder( dst_directory)
293 result = False
294 folder = dst_directory
295 if dlg.run() == gtk.RESPONSE_OK:
296 result = True
297 dst_filename = dlg.get_filename()
298 folder = dlg.get_current_folder()
299 if not dst_filename.endswith( extension):
300 dst_filename += extension
302 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
304 try:
305 shutil.copyfile( src_filename, dst_filename)
306 except:
307 log( 'Error copying file.', sender = self, traceback = True)
309 dlg.destroy()
310 return (result, folder)
313 class gPodder(GladeWidget):
314 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
315 ENTER_URL_TEXT = _('Enter podcast URL...')
317 def new(self):
318 if gpodder.interface == gpodder.MAEMO:
319 # Maemo-specific changes to the UI
320 global scalable_dir
321 scalable_dir = scalable_dir.replace('.svg', '.png')
323 self.app = hildon.Program()
324 gtk.set_application_name('gPodder')
325 self.window = hildon.Window()
326 self.window.connect('delete-event', self.on_gPodder_delete_event)
327 self.window.connect('window-state-event', self.window_state_event)
329 self.itemUpdateChannel.show()
330 self.UpdateChannelSeparator.show()
332 # Give toolbar to the hildon window
333 self.toolbar.parent.remove(self.toolbar)
334 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
335 self.window.add_toolbar(self.toolbar)
337 self.app.add_window(self.window)
338 self.vMain.reparent(self.window)
339 self.gPodder = self.window
341 # Reparent the main menu
342 menu = gtk.Menu()
343 for child in self.mainMenu.get_children():
344 child.reparent(menu)
345 self.itemQuit.reparent(menu)
346 self.window.set_menu(menu)
348 self.mainMenu.destroy()
349 self.window.show()
351 # do some widget hiding
352 self.toolbar.remove(self.toolTransfer)
353 self.itemTransferSelected.hide_all()
354 self.item_email_subscriptions.hide_all()
356 # Feed cache update button
357 self.label120.set_text(_('Update'))
359 # get screen real estate
360 self.hboxContainer.set_border_width(0)
362 self.gPodder.connect('key-press-event', self.on_key_press)
363 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
365 if gl.config.show_url_entry_in_podcast_list:
366 self.hboxAddChannel.show()
368 if not gl.config.show_toolbar:
369 self.toolbar.hide_all()
371 gl.config.add_observer(self.on_config_changed)
372 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
373 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
374 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
375 self.entry_add_channel_unfocus(self.entryAddChannel, None)
377 self.uar = None
378 self.tray_icon = None
380 self.fullscreen = False
381 self.minimized = False
382 self.gPodder.connect('window-state-event', self.window_state_event)
384 self.already_notified_new_episodes = []
385 self.show_hide_tray_icon()
386 self.no_episode_selected.set_sensitive(False)
388 self.itemShowToolbar.set_active(gl.config.show_toolbar)
389 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
391 gl.config.connect_gtk_window( self.gPodder)
392 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
394 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
395 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
396 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
397 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
399 # Make sure we free/close the download queue when we
400 # update the "max downloads" spin button
401 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
402 self.spinMaxDownloads.connect('value-changed', changed_cb)
404 self.default_title = None
405 if app_version.rfind('git') != -1:
406 self.set_title('gPodder %s' % app_version)
407 else:
408 title = self.gPodder.get_title()
409 if title is not None:
410 self.set_title(title)
411 else:
412 self.set_title(_('gPodder'))
414 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
416 # cell renderers for channel tree
417 namecolumn = gtk.TreeViewColumn( _('Podcast'))
419 iconcell = gtk.CellRendererPixbuf()
420 namecolumn.pack_start( iconcell, False)
421 namecolumn.add_attribute( iconcell, 'pixbuf', 5)
422 self.cell_channel_icon = iconcell
424 namecell = gtk.CellRendererText()
425 namecell.set_property('foreground-set', True)
426 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
427 namecolumn.pack_start( namecell, True)
428 namecolumn.add_attribute( namecell, 'markup', 2)
429 namecolumn.add_attribute( namecell, 'foreground', 8)
431 iconcell = gtk.CellRendererPixbuf()
432 iconcell.set_property('xalign', 1.0)
433 namecolumn.pack_start( iconcell, False)
434 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
435 namecolumn.add_attribute(iconcell, 'visible', 7)
436 self.cell_channel_pill = iconcell
438 self.treeChannels.append_column( namecolumn)
439 self.treeChannels.set_headers_visible(False)
441 # enable alternating colors hint
442 self.treeAvailable.set_rules_hint( True)
443 self.treeChannels.set_rules_hint( True)
445 # connect to tooltip signals
446 try:
447 self.treeChannels.set_property('has-tooltip', True)
448 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
449 self.treeAvailable.set_property('has-tooltip', True)
450 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
451 except:
452 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
453 self.last_tooltip_channel = None
454 self.last_tooltip_episode = None
455 self.podcast_list_can_tooltip = True
456 self.episode_list_can_tooltip = True
458 # Add our context menu to treeAvailable
459 if gpodder.interface == gpodder.MAEMO:
460 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
461 else:
462 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
463 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
465 iconcell = gtk.CellRendererPixbuf()
466 if gpodder.interface == gpodder.MAEMO:
467 status_column_label = ''
468 else:
469 status_column_label = _('Status')
470 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
472 namecell = gtk.CellRendererText()
473 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
474 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
475 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
476 namecolumn.set_expand(True)
478 sizecell = gtk.CellRendererText()
479 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
481 releasecell = gtk.CellRendererText()
482 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
484 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
485 itemcolumn.set_reorderable(True)
486 self.treeAvailable.append_column(itemcolumn)
488 if gpodder.interface == gpodder.MAEMO:
489 # Due to screen space contraints, we
490 # hide these columns here by default
491 self.column_size = sizecolumn
492 self.column_released = releasecolumn
493 self.column_released.set_visible(False)
494 self.column_size.set_visible(False)
496 # enable search in treeavailable
497 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
499 # enable multiple selection support
500 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
501 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
503 # columns and renderers for "download progress" tab
504 episodecell = gtk.CellRendererText()
505 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
506 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
507 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
508 episodecolumn.set_expand(True)
510 speedcell = gtk.CellRendererText()
511 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
513 progresscell = gtk.CellRendererProgress()
514 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
515 progresscolumn.set_expand(True)
517 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
518 self.treeDownloads.append_column( itemcolumn)
520 # After we've set up most of the window, show it :)
521 if not gpodder.interface == gpodder.MAEMO:
522 self.gPodder.show()
524 if self.tray_icon:
525 if gl.config.start_iconified:
526 self.iconify_main_window()
527 elif gl.config.minimize_to_tray:
528 self.tray_icon.set_visible(False)
530 services.download_status_manager.register( 'list-changed', self.download_status_updated)
531 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
532 services.cover_downloader.register('cover-available', self.cover_download_finished)
533 services.cover_downloader.register('cover-removed', self.cover_file_removed)
534 self.cover_cache = {}
536 self.treeDownloads.set_model( services.download_status_manager.tree_model)
538 #Add Drag and Drop Support
539 flags = gtk.DEST_DEFAULT_ALL
540 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
541 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
542 self.treeChannels.drag_dest_set( flags, targets, actions)
543 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
545 # Subscribed channels
546 self.active_channel = None
547 self.channels = load_channels()
548 self.update_podcasts_tab()
550 # load list of user applications for audio playback
551 self.user_apps_reader = UserAppsReader(['audio', 'video'])
552 Thread(target=self.read_apps).start()
554 # Clean up old, orphaned download files
555 gl.clean_up_downloads( delete_partial = True)
557 # Set the "Device" menu item for the first time
558 self.update_item_device()
560 # Last folder used for saving episodes
561 self.folder_for_saving_episodes = None
563 # Set up default channel colors
564 self.channel_colors = {
565 'default': None,
566 'updating': gl.config.color_updating_feeds,
567 'parse_error': '#ff0000',
570 # Now, update the feed cache, when everything's in place
571 self.btnUpdateFeeds.show_all()
572 self.updated_feeds = 0
573 self.updating_feed_cache = False
574 self.feed_cache_update_cancelled = False
575 self.update_feed_cache(force_update=gl.config.update_on_startup)
577 # Start the auto-update procedure
578 self.auto_update_procedure(first_run=True)
580 # Delete old episodes if the user wishes to
581 if gl.config.auto_remove_old_episodes:
582 old_episodes = self.get_old_episodes()
583 if len(old_episodes) > 0:
584 self.delete_episode_list(old_episodes, confirm=False)
585 self.updateComboBox()
587 # First-time users should be asked if they want to see the OPML
588 if len(self.channels) == 0:
589 util.idle_add(self.on_itemUpdate_activate, None)
591 def on_tree_channels_resize(self, widget, allocation):
592 if not gl.config.podcast_sidebar_save_space:
593 return
595 window_allocation = self.gPodder.get_allocation()
596 percentage = 100. * float(allocation.width) / float(window_allocation.width)
597 if hasattr(self, 'cell_channel_icon'):
598 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
599 if hasattr(self, 'cell_channel_pill'):
600 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
602 def entry_add_channel_focus(self, widget, event):
603 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
604 if widget.get_text() == self.ENTER_URL_TEXT:
605 widget.set_text('')
607 def entry_add_channel_unfocus(self, widget, event):
608 if widget.get_text() == '':
609 widget.set_text(self.ENTER_URL_TEXT)
610 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
612 def on_config_changed(self, name, old_value, new_value):
613 if name == 'show_toolbar':
614 if new_value:
615 self.toolbar.show_all()
616 else:
617 self.toolbar.hide_all()
618 elif name == 'episode_list_descriptions':
619 self.updateTreeView()
620 elif name == 'show_url_entry_in_podcast_list':
621 if new_value:
622 self.hboxAddChannel.show()
623 else:
624 self.hboxAddChannel.hide()
626 def read_apps(self):
627 time.sleep(3) # give other parts of gpodder a chance to start up
628 self.user_apps_reader.read()
629 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
630 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
632 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
633 # With get_bin_window, we get the window that contains the rows without
634 # the header. The Y coordinate of this window will be the height of the
635 # treeview header. This is the amount we have to subtract from the
636 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
637 (x_bin, y_bin) = treeview.get_bin_window().get_position()
638 y -= x_bin
639 y -= y_bin
640 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
642 if not self.episode_list_can_tooltip:
643 self.last_tooltip_episode = None
644 return False
646 if path is not None:
647 model = treeview.get_model()
648 iter = model.get_iter(path)
649 url = model.get_value(iter, 0)
650 description = model.get_value(iter, 7)
651 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
652 self.last_tooltip_episode = None
653 return False
654 self.last_tooltip_episode = url
656 tooltip.set_text(description)
657 return True
659 self.last_tooltip_episode = None
660 return False
662 def podcast_list_allow_tooltips(self):
663 self.podcast_list_can_tooltip = True
665 def episode_list_allow_tooltips(self):
666 self.episode_list_can_tooltip = True
668 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
669 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
671 if not self.podcast_list_can_tooltip:
672 self.last_tooltip_channel = None
673 return False
675 if path is not None:
676 model = treeview.get_model()
677 iter = model.get_iter(path)
678 url = model.get_value(iter, 0)
679 for channel in self.channels:
680 if channel.url == url:
681 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
682 self.last_tooltip_channel = None
683 return False
684 self.last_tooltip_channel = channel
685 channel.request_save_dir_size()
686 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
687 error_str = model.get_value(iter, 6)
688 if error_str:
689 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
690 error_str = '<span foreground="#ff0000">%s</span>' % error_str
691 table = gtk.Table(rows=3, columns=3)
692 table.set_row_spacings(5)
693 table.set_col_spacings(5)
694 table.set_border_width(5)
696 heading = gtk.Label()
697 heading.set_alignment(0, 1)
698 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
699 table.attach(heading, 0, 1, 0, 1)
700 size_info = gtk.Label()
701 size_info.set_alignment(1, 1)
702 size_info.set_justify(gtk.JUSTIFY_RIGHT)
703 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
704 table.attach(size_info, 2, 3, 0, 1)
706 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
708 if len(channel.description) < 500:
709 description = channel.description
710 else:
711 pos = channel.description.find('\n\n')
712 if pos == -1 or pos > 500:
713 description = channel.description[:498]+'[...]'
714 else:
715 description = channel.description[:pos]
717 description = gtk.Label(description)
718 if error_str:
719 description.set_markup(error_str)
720 description.set_alignment(0, 0)
721 description.set_line_wrap(True)
722 table.attach(description, 0, 3, 2, 3)
724 table.show_all()
725 tooltip.set_custom(table)
727 return True
729 self.last_tooltip_channel = None
730 return False
732 def update_m3u_playlist_clicked(self, widget):
733 self.active_channel.update_m3u_playlist()
734 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
736 def treeview_channels_button_pressed( self, treeview, event):
737 global WEB_BROWSER_ICON
739 if event.button == 3:
740 ( x, y ) = ( int(event.x), int(event.y) )
741 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
743 paths = []
745 # Did the user right-click into a selection?
746 selection = treeview.get_selection()
747 if selection.count_selected_rows() and path:
748 ( model, paths ) = selection.get_selected_rows()
749 if path not in paths:
750 # We have right-clicked, but not into the
751 # selection, assume we don't want to operate
752 # on the selection
753 paths = []
755 # No selection or right click not in selection:
756 # Select the single item where we clicked
757 if not len( paths) and path:
758 treeview.grab_focus()
759 treeview.set_cursor( path, column, 0)
761 ( model, paths ) = ( treeview.get_model(), [ path ] )
763 # We did not find a selection, and the user didn't
764 # click on an item to select -- don't show the menu
765 if not len( paths):
766 return True
768 menu = gtk.Menu()
770 item = gtk.ImageMenuItem( _('Open download folder'))
771 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
772 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
773 menu.append( item)
775 item = gtk.ImageMenuItem( _('Update Feed'))
776 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
777 item.connect('activate', self.on_itemUpdateChannel_activate )
778 item.set_sensitive( not self.updating_feed_cache )
779 menu.append( item)
781 if gl.config.create_m3u_playlists:
782 item = gtk.ImageMenuItem(_('Update M3U playlist'))
783 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
784 item.connect('activate', self.update_m3u_playlist_clicked)
785 menu.append(item)
787 if self.active_channel.link:
788 item = gtk.ImageMenuItem(_('Visit website'))
789 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
790 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
791 menu.append(item)
793 menu.append( gtk.SeparatorMenuItem())
795 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
796 item.connect( 'activate', self.on_itemEditChannel_activate)
797 menu.append( item)
799 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
800 item.connect( 'activate', self.on_itemRemoveChannel_activate)
801 menu.append( item)
803 menu.show_all()
804 # Disable tooltips while we are showing the menu, so
805 # the tooltip will not appear over the menu
806 self.podcast_list_can_tooltip = False
807 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
808 menu.popup( None, None, None, event.button, event.time)
810 return True
812 def on_itemClose_activate(self, widget):
813 if self.tray_icon is not None:
814 if gpodder.interface == gpodder.MAEMO:
815 self.gPodder.set_property('visible', False)
816 else:
817 self.iconify_main_window()
818 else:
819 self.on_gPodder_delete_event(widget)
821 def cover_file_removed(self, channel_url):
823 The Cover Downloader calls this when a previously-
824 available cover has been removed from the disk. We
825 have to update our cache to reflect this change.
827 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
828 for row in self.treeChannels.get_model():
829 if row[COLUMN_URL] == channel_url:
830 row[COLUMN_PIXBUF] = None
831 key = (channel_url, gl.config.podcast_list_icon_size, \
832 gl.config.podcast_list_icon_size)
833 if key in self.cover_cache:
834 del self.cover_cache[key]
837 def cover_download_finished(self, channel_url, pixbuf):
839 The Cover Downloader calls this when it has finished
840 downloading (or registering, if already downloaded)
841 a new channel cover, which is ready for displaying.
843 if pixbuf is not None:
844 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
845 for row in self.treeChannels.get_model():
846 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
847 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)
848 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
850 def save_episode_as_file( self, url, *args):
851 episode = self.active_channel.find_episode(url)
853 folder = self.folder_for_saving_episodes
854 (result, folder) = self.show_copy_dialog(src_filename=episode.local_filename(), dst_filename=episode.sync_filename(), dst_directory=folder)
855 self.folder_for_saving_episodes = folder
857 def copy_episode_bluetooth(self, url, *args):
858 episode = self.active_channel.find_episode(url)
859 filename = episode.local_filename()
861 if gl.config.bluetooth_ask_always:
862 device = None
863 else:
864 device = gl.config.bluetooth_device_address
866 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
867 (base, ext) = os.path.splitext(filename)
868 if not destfile.endswith(ext):
869 destfile += ext
871 if gl.config.bluetooth_use_converter:
872 title = _('Converting file')
873 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
874 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
875 dlg.set_title(title)
876 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
877 dlg.show_all()
878 else:
879 dlg = None
881 def convert_and_send_thread(filename, destfile, device, dialog, notify):
882 if gl.config.bluetooth_use_converter:
883 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
884 result = p.wait()
885 if dialog is not None:
886 dialog.destroy()
887 else:
888 try:
889 shutil.copyfile(filename, destfile)
890 result = 0
891 except:
892 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
893 result = 1
895 if result == 0 or not os.path.exists(destfile):
896 util.bluetooth_send_file(destfile, device)
897 else:
898 notify(_('Error converting file.'), _('Bluetooth file transfer'))
899 util.delete_file(destfile)
901 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
903 def treeview_button_pressed( self, treeview, event):
904 global WEB_BROWSER_ICON
906 # Use right-click for the Desktop version and left-click for Maemo
907 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
908 (event.button == 3 and gpodder.interface == gpodder.GUI):
909 ( x, y ) = ( int(event.x), int(event.y) )
910 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
912 paths = []
914 # Did the user right-click into a selection?
915 selection = self.treeAvailable.get_selection()
916 if selection.count_selected_rows() and path:
917 ( model, paths ) = selection.get_selected_rows()
918 if path not in paths:
919 # We have right-clicked, but not into the
920 # selection, assume we don't want to operate
921 # on the selection
922 paths = []
924 # No selection or right click not in selection:
925 # Select the single item where we clicked
926 if not len( paths) and path:
927 treeview.grab_focus()
928 treeview.set_cursor( path, column, 0)
930 ( model, paths ) = ( treeview.get_model(), [ path ] )
932 # We did not find a selection, and the user didn't
933 # click on an item to select -- don't show the menu
934 if not len( paths):
935 return True
937 first_url = model.get_value( model.get_iter( paths[0]), 0)
938 episode = db.load_episode(first_url)
940 menu = gtk.Menu()
942 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
944 if can_play:
945 if open_instead_of_play:
946 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
947 else:
948 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
949 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
950 menu.append(self.set_finger_friendly(item))
952 if not episode['is_locked'] and can_delete:
953 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
954 item.connect('activate', self.on_btnDownloadedDelete_clicked)
955 menu.append(self.set_finger_friendly(item))
957 if can_cancel:
958 item = gtk.ImageMenuItem( _('Cancel download'))
959 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
960 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
961 menu.append(self.set_finger_friendly(item))
963 if can_download:
964 item = gtk.ImageMenuItem(_('Download'))
965 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
966 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
967 menu.append(self.set_finger_friendly(item))
969 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
970 item = gtk.ImageMenuItem(_('Do not download'))
971 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
972 item.connect('activate', lambda w: self.mark_selected_episodes_old())
973 menu.append(self.set_finger_friendly(item))
974 elif episode['state'] == db.STATE_NORMAL and can_download:
975 item = gtk.ImageMenuItem(_('Mark as new'))
976 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
977 item.connect('activate', lambda w: self.mark_selected_episodes_new())
978 menu.append(self.set_finger_friendly(item))
980 if can_play and not can_download:
981 menu.append( gtk.SeparatorMenuItem())
982 item = gtk.ImageMenuItem(_('Save to disk'))
983 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
984 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
985 menu.append(self.set_finger_friendly(item))
986 if gl.config.bluetooth_enabled:
987 item = gtk.ImageMenuItem(_('Send via bluetooth'))
988 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
989 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
990 menu.append(self.set_finger_friendly(item))
991 if can_transfer:
992 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
993 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
994 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
995 menu.append(self.set_finger_friendly(item))
997 if can_play:
998 menu.append( gtk.SeparatorMenuItem())
999 is_played = episode['is_played']
1000 if is_played:
1001 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1002 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1003 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1004 menu.append(self.set_finger_friendly(item))
1005 else:
1006 item = gtk.ImageMenuItem(_('Mark as played'))
1007 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1008 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1009 menu.append(self.set_finger_friendly(item))
1011 is_locked = episode['is_locked']
1012 if is_locked:
1013 item = gtk.ImageMenuItem(_('Allow deletion'))
1014 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1015 item.connect('activate', self.on_item_toggle_lock_activate)
1016 menu.append(self.set_finger_friendly(item))
1017 else:
1018 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1019 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1020 item.connect('activate', self.on_item_toggle_lock_activate)
1021 menu.append(self.set_finger_friendly(item))
1023 if len(paths) == 1:
1024 menu.append(gtk.SeparatorMenuItem())
1025 # Single item, add episode information menu item
1026 episode_url = model.get_value( model.get_iter( paths[0]), 0)
1027 item = gtk.ImageMenuItem(_('Episode details'))
1028 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1029 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1030 menu.append(self.set_finger_friendly(item))
1031 episode = self.active_channel.find_episode(episode_url)
1032 # If we have it, also add episode website link
1033 if episode and episode.link and episode.link != episode.url:
1034 item = gtk.ImageMenuItem(_('Visit website'))
1035 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1036 item.connect('activate', lambda w: util.open_website(episode.link))
1037 menu.append(self.set_finger_friendly(item))
1039 if gpodder.interface == gpodder.MAEMO:
1040 # Because we open the popup on left-click for Maemo,
1041 # we also include a non-action to close the menu
1042 menu.append(gtk.SeparatorMenuItem())
1043 item = gtk.ImageMenuItem(_('Close this menu'))
1044 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1045 menu.append(self.set_finger_friendly(item))
1047 menu.show_all()
1048 # Disable tooltips while we are showing the menu, so
1049 # the tooltip will not appear over the menu
1050 self.episode_list_can_tooltip = False
1051 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1052 menu.popup( None, None, None, event.button, event.time)
1054 return True
1056 def set_title(self, new_title):
1057 self.default_title = new_title
1058 self.gPodder.set_title(new_title)
1060 def download_progress_updated( self, count, percentage):
1061 title = [ self.default_title ]
1063 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1065 if count == 1:
1066 title.append( _('downloading one file'))
1067 elif count > 1:
1068 title.append( _('downloading %d files') % count)
1070 if len(title) == 2:
1071 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1073 self.gPodder.set_title( ' - '.join( title))
1075 # Have all the downloads completed?
1076 # If so execute user command if defined, else do nothing
1077 if count == 0:
1078 if len(gl.config.cmd_all_downloads_complete) > 0:
1079 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1081 def playback_episode(self, episode, stream=False):
1082 (success, application) = gl.playback_episode(episode, stream)
1083 if not success:
1084 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), ))
1085 self.updateComboBox(only_selected_channel=True)
1087 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1088 if model is None:
1089 return True
1091 key = key.lower()
1093 # columns, as defined in libpodcasts' get model method
1094 # 1 = episode title, 7 = description
1095 columns = (1, 7)
1097 for column in columns:
1098 value = model.get_value( iter, column).lower()
1099 if value.find( key) != -1:
1100 return False
1102 return True
1104 def change_menu_item(self, menuitem, icon=None, label=None):
1105 if icon is not None:
1106 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1107 if label is not None:
1108 label_widget = menuitem.get_child()
1109 label_widget.set_text(label)
1111 def play_or_download(self):
1112 if self.wNotebook.get_current_page() > 0:
1113 return
1115 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1116 ( is_played, is_locked ) = (False,)*2
1118 open_instead_of_play = False
1120 selection = self.treeAvailable.get_selection()
1121 if selection.count_selected_rows() > 0:
1122 (model, paths) = selection.get_selected_rows()
1124 for path in paths:
1125 url = model.get_value( model.get_iter( path), 0)
1126 local_filename = model.get_value( model.get_iter( path), 8)
1128 episode = podcastItem.load(url, self.active_channel)
1130 if episode.file_type() not in ('audio', 'video'):
1131 open_instead_of_play = True
1133 if episode.was_downloaded():
1134 can_play = episode.was_downloaded(and_exists=True)
1135 can_delete = True
1136 is_played = episode.is_played
1137 is_locked = episode.is_locked
1138 if not can_play:
1139 can_download = True
1140 else:
1141 if services.download_status_manager.is_download_in_progress(url):
1142 can_cancel = True
1143 else:
1144 can_download = True
1146 can_download = can_download and not can_cancel
1147 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1148 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1150 if open_instead_of_play:
1151 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1152 can_transfer = False
1153 else:
1154 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1156 self.toolPlay.set_sensitive( can_play)
1157 self.toolDownload.set_sensitive( can_download)
1158 self.toolTransfer.set_sensitive( can_transfer)
1159 self.toolCancel.set_sensitive( can_cancel)
1161 if can_cancel:
1162 self.item_cancel_download.show_all()
1163 else:
1164 self.item_cancel_download.hide_all()
1165 if can_download:
1166 self.itemDownloadSelected.show_all()
1167 else:
1168 self.itemDownloadSelected.hide_all()
1169 if can_play:
1170 if open_instead_of_play:
1171 self.itemOpenSelected.show_all()
1172 self.itemPlaySelected.hide_all()
1173 else:
1174 self.itemPlaySelected.show_all()
1175 self.itemOpenSelected.hide_all()
1176 if not can_download:
1177 self.itemDeleteSelected.show_all()
1178 else:
1179 self.itemDeleteSelected.hide_all()
1180 self.item_toggle_played.show_all()
1181 self.item_toggle_lock.show_all()
1182 self.separator9.show_all()
1183 if is_played:
1184 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1185 else:
1186 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1187 if is_locked:
1188 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1189 else:
1190 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1191 else:
1192 self.itemPlaySelected.hide_all()
1193 self.itemOpenSelected.hide_all()
1194 self.itemDeleteSelected.hide_all()
1195 self.item_toggle_played.hide_all()
1196 self.item_toggle_lock.hide_all()
1197 self.separator9.hide_all()
1198 if can_play or can_download or can_cancel:
1199 self.item_episode_details.show_all()
1200 self.separator16.show_all()
1201 self.no_episode_selected.hide_all()
1202 else:
1203 self.item_episode_details.hide_all()
1204 self.separator16.hide_all()
1205 self.no_episode_selected.show_all()
1207 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1209 def download_status_updated( self):
1210 count = services.download_status_manager.count()
1211 if count:
1212 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1213 else:
1214 self.labelDownloads.set_text( _('Downloads'))
1216 self.updateComboBox()
1218 def on_cbMaxDownloads_toggled(self, widget, *args):
1219 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1221 def on_cbLimitDownloads_toggled(self, widget, *args):
1222 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1224 def episode_new_status_changed(self):
1225 self.updateComboBox()
1226 self.updateTreeView()
1228 def updateComboBox(self, selected_url=None, only_selected_channel=False):
1229 (model, iter) = self.treeChannels.get_selection().get_selected()
1231 if only_selected_channel:
1232 if iter and self.active_channel is not None:
1233 update_channel_model_by_iter( self.treeChannels.get_model(),
1234 iter, self.active_channel, self.channel_colors,
1235 self.cover_cache, *(gl.config.podcast_list_icon_size,)*2 )
1236 else:
1237 if model and iter and selected_url is None:
1238 # Get the URL of the currently-selected podcast
1239 selected_url = model.get_value(iter, 0)
1241 rect = self.treeChannels.get_visible_rect()
1242 self.treeChannels.set_model( channels_to_model( self.channels,
1243 self.channel_colors, self.cover_cache,
1244 *(gl.config.podcast_list_icon_size,)*2 ))
1245 util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
1247 try:
1248 selected_path = (0,)
1249 # Find the previously-selected URL in the new
1250 # model if we have an URL (else select first)
1251 if selected_url is not None:
1252 model = self.treeChannels.get_model()
1253 pos = model.get_iter_first()
1254 while pos is not None:
1255 url = model.get_value(pos, 0)
1256 if url == selected_url:
1257 selected_path = model.get_path(pos)
1258 break
1259 pos = model.iter_next(pos)
1261 self.treeChannels.get_selection().select_path(selected_path)
1262 except:
1263 log( 'Cannot set selection on treeChannels', sender = self)
1264 self.on_treeChannels_cursor_changed( self.treeChannels)
1266 def updateTreeView( self):
1267 if self.channels and self.active_channel is not None:
1268 self.treeAvailable.set_model(self.active_channel.tree_model)
1269 self.treeAvailable.columns_autosize()
1270 self.play_or_download()
1271 else:
1272 if self.treeAvailable.get_model():
1273 self.treeAvailable.get_model().clear()
1275 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1276 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1278 dnd_channel = None
1279 if path is not None:
1280 model = self.treeChannels.get_model()
1281 iter = model.get_iter(path)
1282 url = model.get_value(iter, 0)
1283 for channel in self.channels:
1284 if channel.url == url:
1285 dnd_channel = channel
1286 break
1288 result = sel.data
1289 rl = result.strip().lower()
1290 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1291 services.cover_downloader.replace_cover(dnd_channel, result)
1292 else:
1293 self.add_new_channel(result)
1295 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1296 result = util.normalize_feed_url( result)
1298 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1299 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1300 waitdlg.set_title(_('Downloading episode list'))
1301 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1302 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1303 waitpb = gtk.ProgressBar()
1304 if block:
1305 waitdlg.vbox.add(waitpb)
1306 waitdlg.show_all()
1307 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1309 if not result:
1310 title = _('URL scheme not supported')
1311 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1312 self.show_message( message, title)
1313 return
1315 for old_channel in self.channels:
1316 if old_channel.url == result:
1317 log( 'Channel already exists: %s', result)
1318 # Select the existing channel in combo box
1319 for i in range( len( self.channels)):
1320 if self.channels[i] == old_channel:
1321 self.treeChannels.get_selection().select_path( (i,))
1322 self.on_treeChannels_cursor_changed(self.treeChannels)
1323 break
1324 self.show_message( _('You have already subscribed to this podcast: %s') % (
1325 saxutils.escape( old_channel.title), ), _('Already added'))
1326 waitdlg.destroy()
1327 return
1329 self.entryAddChannel.set_text(_('Downloading feed...'))
1330 self.entryAddChannel.set_sensitive(False)
1331 self.btnAddChannel.set_sensitive(False)
1332 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1333 thread = Thread( target=self.add_new_channel_proc, args=args )
1334 thread.start()
1336 while block and thread.isAlive():
1337 while gtk.events_pending():
1338 gtk.main_iteration( False)
1339 waitpb.pulse()
1340 time.sleep(0.05)
1343 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1344 log( 'Adding new channel: %s', url)
1345 channel = error = None
1346 try:
1347 channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1348 except HTTPAuthError, e:
1349 error = e
1350 except Exception, e:
1351 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1353 util.idle_add( callback, channel, url, error, *callback_args )
1355 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1356 if channel is not None:
1357 self.channels.append( channel)
1358 save_channels( self.channels)
1359 if not quiet:
1360 # download changed channels and select the new episode in the UI afterwards
1361 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1363 (username, password) = util.username_password_from_url( url)
1364 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')):
1365 channel.username = username
1366 channel.password = password
1367 log('Saving authentication data for episode downloads..', sender = self)
1368 channel.save()
1369 # We need to update the channel list otherwise the authentication
1370 # data won't show up in the channel editor.
1371 # TODO: Only updated the newly added feed to save some cpu cycles
1372 self.channels = load_channels()
1374 if ask_download_new:
1375 new_episodes = channel.get_new_episodes()
1376 if len(new_episodes):
1377 self.new_episodes_show(new_episodes)
1379 elif isinstance( error, HTTPAuthError ):
1380 response, auth_tokens = self.UsernamePasswordDialog(
1381 _('Feed requires authentication'), _('Please enter your username and password.'))
1383 if response:
1384 self.add_new_channel( url, authentication_tokens=auth_tokens )
1386 else:
1387 # Ok, the URL is not a channel, or there is some other
1388 # error - let's see if it's a web page or OPML file...
1389 try:
1390 data = urllib2.urlopen(url).read().lower()
1391 if '</opml>' in data:
1392 # This looks like an OPML feed
1393 self.on_item_import_from_file_activate(None, url)
1395 elif '</html>' in data:
1396 # This looks like a web page
1397 title = _('The URL is a website')
1398 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.)')
1399 if self.show_confirmation(message, title):
1400 util.open_website(url)
1402 except Exception, e:
1403 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1405 title = _('Error adding podcast')
1406 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1407 self.show_message( message, title)
1409 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1410 self.entryAddChannel.set_sensitive(True)
1411 self.btnAddChannel.set_sensitive(True)
1412 self.update_podcasts_tab()
1413 waitdlg.destroy()
1416 def update_feed_cache_finish_callback(self, channels=None,
1417 notify_no_new_episodes=False, select_url_afterwards=None):
1419 db.commit()
1421 self.updating_feed_cache = False
1422 self.hboxUpdateFeeds.hide_all()
1423 self.btnUpdateFeeds.show_all()
1424 self.itemUpdate.set_sensitive(True)
1425 self.itemUpdateChannel.set_sensitive(True)
1427 # If we want to select a specific podcast (via its URL)
1428 # after the update, we give it to updateComboBox here to
1429 # select exactly this podcast after updating the view
1430 self.updateComboBox(selected_url=select_url_afterwards)
1432 if self.tray_icon:
1433 self.tray_icon.set_status(None)
1434 if self.minimized:
1435 new_episodes = []
1436 # look for new episodes to notify
1437 for channel in self.channels:
1438 for episode in channel.get_new_episodes():
1439 if not episode in self.already_notified_new_episodes:
1440 new_episodes.append(episode)
1441 self.already_notified_new_episodes.append(episode)
1442 # notify new episodes
1444 if len(new_episodes) == 0:
1445 if notify_no_new_episodes and self.tray_icon is not None:
1446 msg = _('No new episodes available for download')
1447 self.tray_icon.send_notification(msg)
1448 return
1449 elif len(new_episodes) == 1:
1450 title = _('gPodder has found %s') % (_('one new episode:'),)
1451 else:
1452 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1453 message = self.tray_icon.format_episode_list(new_episodes)
1455 #auto download new episodes
1456 if gl.config.auto_download_when_minimized:
1457 message += '\n<i>(%s...)</i>' % _('downloading')
1458 self.download_episode_list(new_episodes)
1459 self.tray_icon.send_notification(message, title)
1460 return
1462 # open the episodes selection dialog
1463 self.channels = load_channels()
1464 self.updateComboBox()
1465 if not self.feed_cache_update_cancelled:
1466 self.download_all_new(channels=channels)
1468 def update_feed_cache_callback(self, progressbar, title, position, count):
1469 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1470 progressbar.set_text(progression)
1471 if self.tray_icon:
1472 self.tray_icon.set_status(
1473 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1474 if count > 0:
1475 progressbar.set_fraction(float(position)/float(count))
1477 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1478 callback_proc, finish_proc):
1480 semaphore.acquire()
1481 if not self.feed_cache_update_cancelled:
1482 try:
1483 channel.update()
1484 except:
1485 log('Darn SQLite LOCK!', sender=self, traceback=True)
1487 # By the time we get here the update may have already been cancelled
1488 if not self.feed_cache_update_cancelled:
1489 callback_proc(channel.title, self.updated_feeds, total_channels)
1491 self.updated_feeds += 1
1492 self.treeview_channel_set_color( channel, 'default' )
1493 channel.update_flag = False
1495 semaphore.release()
1496 if self.updated_feeds == total_channels:
1497 finish_proc()
1499 def on_btnCancelFeedUpdate_clicked(self, widget):
1500 self.pbFeedUpdate.set_text(_('Cancelling...'))
1501 self.feed_cache_update_cancelled = True
1503 def update_feed_cache(self, channels=None, force_update=True,
1504 notify_no_new_episodes=False, select_url_afterwards=None):
1506 if self.updating_feed_cache:
1507 return
1509 if not force_update:
1510 self.channels = load_channels()
1511 self.updateComboBox()
1512 return
1514 self.updating_feed_cache = True
1515 self.itemUpdate.set_sensitive(False)
1516 self.itemUpdateChannel.set_sensitive(False)
1518 if self.tray_icon:
1519 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1521 if channels is None:
1522 channels = self.channels
1524 if len(channels) == 1:
1525 text = _('Updating %d feed.')
1526 else:
1527 text = _('Updating %d feeds.')
1528 self.pbFeedUpdate.set_text( text % len(channels))
1529 self.pbFeedUpdate.set_fraction(0)
1531 # let's get down to business..
1532 callback_proc = lambda title, pos, count: util.idle_add(
1533 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1534 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1535 channels, notify_no_new_episodes, select_url_afterwards )
1537 self.updated_feeds = 0
1538 self.feed_cache_update_cancelled = False
1539 self.btnUpdateFeeds.hide_all()
1540 self.hboxUpdateFeeds.show_all()
1541 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1543 for channel in channels:
1544 self.treeview_channel_set_color( channel, 'updating' )
1545 channel.update_flag = True
1546 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1547 thread = Thread( target = self.update_feed_cache_proc, args = args)
1548 thread.start()
1550 def treeview_channel_set_color( self, channel, color ):
1551 if self.treeChannels.get_model():
1552 if color in self.channel_colors:
1553 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1554 else:
1555 self.treeChannels.get_model().set(channel.iter, 8, color)
1557 def on_gPodder_delete_event(self, widget, *args):
1558 """Called when the GUI wants to close the window
1559 Displays a confirmation dialog (and closes/hides gPodder)
1562 downloading = services.download_status_manager.has_items()
1564 # Only iconify if we are using the window's "X" button,
1565 # but not when we are using "Quit" in the menu or toolbar
1566 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1567 self.iconify_main_window()
1568 elif gl.config.on_quit_ask or downloading:
1569 if gpodder.interface == gpodder.MAEMO:
1570 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1571 if result:
1572 self.close_gpodder()
1573 else:
1574 return True
1575 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1576 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1577 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1579 title = _('Quit gPodder')
1580 if downloading:
1581 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1582 else:
1583 message = _('Do you really want to quit gPodder now?')
1585 dialog.set_title(title)
1586 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1587 if not downloading:
1588 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1589 dialog.vbox.pack_start(cb_ask)
1590 cb_ask.show_all()
1592 result = dialog.run()
1593 dialog.destroy()
1595 if result == gtk.RESPONSE_CLOSE:
1596 if not downloading and cb_ask.get_active() == True:
1597 gl.config.on_quit_ask = False
1598 self.close_gpodder()
1599 else:
1600 self.close_gpodder()
1602 return True
1604 def close_gpodder(self):
1605 """ clean everything and exit properly
1607 if self.channels:
1608 if not save_channels(self.channels):
1609 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1611 services.download_status_manager.cancel_all()
1612 db.commit()
1614 self.gtk_main_quit()
1615 sys.exit( 0)
1617 def get_old_episodes(self):
1618 episodes = []
1619 for channel in self.channels:
1620 for episode in channel.get_downloaded_episodes():
1621 if episode.is_old() and not episode.is_locked and episode.is_played:
1622 episodes.append(episode)
1623 return episodes
1625 def for_each_selected_episode_url( self, callback):
1626 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1627 for path in paths:
1628 url = model.get_value( model.get_iter( path), 0)
1629 try:
1630 callback( url)
1631 except Exception, e:
1632 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1634 self.updateComboBox(only_selected_channel=True)
1636 def delete_episode_list( self, episodes, confirm = True):
1637 if len(episodes) == 0:
1638 return
1640 if len(episodes) == 1:
1641 message = _('Do you really want to delete this episode?')
1642 else:
1643 message = _('Do you really want to delete %d episodes?') % len(episodes)
1645 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1646 return
1648 for episode in episodes:
1649 log('Deleting episode: %s', episode.title, sender = self)
1650 episode.delete_from_disk()
1652 self.download_status_updated()
1654 def on_itemRemoveOldEpisodes_activate( self, widget):
1655 columns = (
1656 ('title_and_description', None, None, _('Episode')),
1657 ('channel_prop', None, None, _('Podcast')),
1658 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1659 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1660 ('played_prop', None, None, _('Status')),
1661 ('age_prop', None, None, _('Downloaded')),
1664 selection_buttons = {
1665 _('Select played'): lambda episode: episode.is_played,
1666 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1669 instructions = _('Select the episodes you want to delete from your hard disk.')
1671 episodes = []
1672 selected = []
1673 for channel in self.channels:
1674 for episode in channel.get_downloaded_episodes():
1675 if not episode.is_locked:
1676 episodes.append(episode)
1677 selected.append(episode.is_played)
1679 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1680 episodes = episodes, selected = selected, columns = columns, \
1681 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1682 selection_buttons = selection_buttons)
1684 def mark_selected_episodes_new(self):
1685 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1686 self.for_each_selected_episode_url(callback)
1688 def mark_selected_episodes_old(self):
1689 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1690 self.for_each_selected_episode_url(callback)
1692 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1693 if toggle:
1694 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1695 else:
1696 callback = lambda url: db.mark_episode(url, is_played=new_value)
1698 self.for_each_selected_episode_url(callback)
1700 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1701 if toggle:
1702 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1703 else:
1704 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1706 self.for_each_selected_episode_url(callback)
1708 def on_item_email_subscriptions_activate(self, widget):
1709 if not self.channels:
1710 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1711 elif not gl.send_subscriptions():
1712 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1714 def on_itemUpdateChannel_activate(self, widget=None):
1715 self.update_feed_cache(channels=[self.active_channel,])
1717 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1718 restore_from = can_restore_from_opml()
1720 if self.channels:
1721 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1722 elif restore_from is not None:
1723 title = _('Database upgrade required')
1724 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?')
1725 if self.show_confirmation(message, title):
1726 add_callback = lambda url: self.add_new_channel(url, False, True)
1727 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1728 w.set_has_separator(False)
1729 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1730 w.set_default_size(500, -1)
1731 pb = gtk.ProgressBar()
1732 l = gtk.Label()
1733 l.set_padding(6, 3)
1734 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1735 l.set_alignment(0.0, 0.5)
1736 w.vbox.pack_start(l)
1737 l = gtk.Label()
1738 l.set_padding(6, 3)
1739 l.set_alignment(0.0, 0.5)
1740 l.set_text(_('Please wait while your settings are converted.'))
1741 w.vbox.pack_start(l)
1742 w.vbox.pack_start(pb)
1743 lb = gtk.Label()
1744 lb.set_ellipsize(pango.ELLIPSIZE_END)
1745 lb.set_alignment(0.0, 0.5)
1746 lb.set_padding(6, 6)
1747 w.vbox.pack_start(lb)
1749 def set_pb_status(pb, lb, fraction, text):
1750 pb.set_fraction(float(fraction)/100.0)
1751 pb.set_text('%.0f %%' % fraction)
1752 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1753 while gtk.events_pending():
1754 gtk.main_iteration(False)
1755 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1756 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1757 w.show_all()
1758 start = datetime.datetime.now()
1759 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1760 # Refresh the view with the updated episodes
1761 self.updateComboBox()
1762 time_taken = str(datetime.datetime.now()-start)
1763 status_callback(100.0, _('Migration finished in %s') % time_taken)
1764 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1765 w.run()
1766 w.destroy()
1767 else:
1768 title = _('Import podcasts from the web')
1769 message = _('Your podcast list is empty. Do you want to see a list of example podcasts you can subscribe to?')
1770 if self.show_confirmation(message, title):
1771 self.on_itemImportChannels_activate(self, widget)
1773 def download_episode_list( self, episodes):
1774 services.download_status_manager.start_batch_mode()
1775 for episode in episodes:
1776 log('Downloading episode: %s', episode.title, sender = self)
1777 filename = episode.local_filename()
1778 if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
1779 download.DownloadThread( episode.channel, episode, self.notification).start()
1780 services.download_status_manager.end_batch_mode()
1782 def new_episodes_show(self, episodes):
1783 columns = (
1784 ('title_and_description', None, None, _('Episode')),
1785 ('channel_prop', None, None, _('Podcast')),
1786 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1787 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1790 if len(episodes) > 0:
1791 instructions = _('Select the episodes you want to download now.')
1793 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1794 episodes=episodes, columns=columns, selected_default=True, \
1795 stock_ok_button = 'gpodder-download', \
1796 callback=self.download_episode_list, \
1797 remove_callback=lambda e: e.mark_old(), \
1798 remove_action=_('Never download'), \
1799 remove_finished=self.episode_new_status_changed)
1800 else:
1801 title = _('No new episodes')
1802 message = _('No new episodes to download.\nPlease check for new episodes later.')
1803 self.show_message(message, title)
1805 def on_itemDownloadAllNew_activate(self, widget, *args):
1806 self.download_all_new()
1808 def download_all_new(self, channels=None):
1809 if channels is None:
1810 channels = self.channels
1811 episodes = []
1812 for channel in channels:
1813 for episode in channel.get_new_episodes():
1814 episodes.append(episode)
1815 self.new_episodes_show(episodes)
1817 def get_all_episodes(self, exclude_nonsignificant=True ):
1818 """'exclude_nonsignificant' will exclude non-downloaded episodes
1819 and all episodes from channels that are set to skip when syncing"""
1820 episode_list = []
1821 for channel in self.channels:
1822 if not channel.sync_to_devices and exclude_nonsignificant:
1823 log('Skipping channel: %s', channel.title, sender=self)
1824 continue
1825 for episode in channel.get_all_episodes():
1826 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1827 episode_list.append(episode)
1828 return episode_list
1830 def ipod_delete_played(self, device):
1831 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1832 episodes_on_device = device.get_all_tracks()
1833 for local_episode in all_episodes:
1834 device_episode = device.episode_on_device(local_episode)
1835 if device_episode and ( local_episode.is_played and not local_episode.is_locked
1836 or local_episode.state == db.STATE_DELETED ):
1837 log("mp3_player_delete_played: removing %s" % device_episode.title)
1838 device.remove_track(device_episode)
1840 def on_sync_to_ipod_activate(self, widget, episodes=None):
1841 # make sure gpod is available before even trying to sync
1842 if gl.config.device_type == 'ipod' and not sync.gpod_available:
1843 title = _('Cannot Sync To iPod')
1844 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
1845 self.notification( message, title )
1846 return
1847 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
1848 title = _('Cannot sync to MTP device')
1849 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
1850 self.notification( message, title )
1851 return
1853 device = sync.open_device()
1854 device.register( 'post-done', self.sync_to_ipod_completed )
1856 if device is None:
1857 title = _('No device configured')
1858 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1859 self.notification(message, title)
1860 return
1862 if not device.open():
1863 title = _('Cannot open device')
1864 message = _('There has been an error opening your device.')
1865 self.notification(message, title)
1866 return
1868 sync_all_episodes = not bool(episodes)
1870 if episodes is None:
1871 episodes = self.get_all_episodes()
1873 # make sure we have enough space on the device
1874 total_size = 0
1875 free_space = device.get_free_space()
1876 for episode in episodes:
1877 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
1878 total_size += util.calculate_size(str(episode.local_filename()))
1880 if total_size > free_space:
1881 # can be negative because of the 10 MiB for reserved for the iTunesDB
1882 free_space = max( free_space, 0 )
1883 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
1884 title = _('Not enough space left on device.')
1885 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
1886 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
1887 self.notification(message, title)
1888 else:
1889 # start syncing!
1890 gPodderSync(device=device, gPodder=self)
1891 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
1892 if self.tray_icon:
1893 self.tray_icon.set_synchronisation_device(device)
1895 def sync_to_ipod_completed(self, device, successful_sync):
1896 device.unregister( 'post-done', self.sync_to_ipod_completed )
1898 if self.tray_icon:
1899 self.tray_icon.release_synchronisation_device()
1901 if not successful_sync:
1902 title = _('Error closing device')
1903 message = _('There has been an error closing your device.')
1904 self.notification(message, title)
1906 # update model for played state updates after sync
1907 util.idle_add(self.updateComboBox)
1909 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
1910 if sync_all_episodes:
1911 device.add_tracks(episodes)
1912 # 'only_sync_not_played' must be used or else all the played
1913 # tracks will be copied then immediately deleted
1914 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
1915 self.ipod_delete_played(device)
1916 else:
1917 device.add_tracks(episodes, force_played=True)
1918 device.close()
1920 def ipod_cleanup_callback(self, device, tracks):
1921 title = _('Delete podcasts from device?')
1922 message = _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
1923 if len(tracks) > 0 and self.show_confirmation(message, title):
1924 device.remove_tracks(tracks)
1926 if not device.close():
1927 title = _('Error closing device')
1928 message = _('There has been an error closing your device.')
1929 self.show_message(message, title)
1930 return
1932 def on_cleanup_ipod_activate(self, widget, *args):
1933 columns = (
1934 ('title', None, None, _('Episode')),
1935 ('podcast', None, None, _('Podcast')),
1936 ('filesize', None, None, _('Size')),
1937 ('modified', None, None, _('Copied')),
1938 ('playcount', None, None, _('Play count')),
1939 ('released', None, None, _('Released')),
1942 device = sync.open_device()
1944 if device is None:
1945 title = _('No device configured')
1946 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1947 self.show_message(message, title)
1948 return
1950 if not device.open():
1951 title = _('Cannot open device')
1952 message = _('There has been an error opening your device.')
1953 self.show_message(message, title)
1954 return
1956 gPodderSync(device=device, gPodder=self)
1958 tracks = device.get_all_tracks()
1959 if len(tracks) > 0:
1960 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1961 wanted_columns = []
1962 for key, sort_name, sort_type, caption in columns:
1963 want_this_column = False
1964 for track in tracks:
1965 if getattr(track, key) is not None:
1966 want_this_column = True
1967 break
1969 if want_this_column:
1970 wanted_columns.append((key, sort_name, sort_type, caption))
1971 title = _('Remove podcasts from device')
1972 instructions = _('Select the podcast episodes you want to remove from your device.')
1973 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
1974 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
1975 else:
1976 title = _('No files on device')
1977 message = _('The devices contains no files to be removed.')
1978 self.show_message(message, title)
1979 device.close()
1981 def on_manage_device_playlist(self, widget):
1982 # make sure gpod is available before even trying to sync
1983 if gl.config.device_type == 'ipod' and not sync.gpod_available:
1984 title = _('Cannot manage iPod playlist')
1985 message = _('This feature is not available for iPods.')
1986 self.notification( message, title )
1987 return
1988 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
1989 title = _('Cannot manage MTP device playlist')
1990 message = _('This feature is not available for MTP devices.')
1991 self.notification( message, title )
1992 return
1994 device = sync.open_device()
1996 if device is None:
1997 title = _('No device configured')
1998 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
1999 self.notification(message, title)
2000 return
2002 if not device.open():
2003 title = _('Cannot open device')
2004 message = _('There has been an error opening your device.')
2005 self.notification(message, title)
2006 return
2008 gPodderPlaylist(device=device, gPodder=self)
2009 device.close()
2011 def show_hide_tray_icon(self):
2012 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2013 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
2014 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2015 self.tray_icon.set_visible(False)
2016 del self.tray_icon
2017 self.tray_icon = None
2019 if gl.config.minimize_to_tray and self.tray_icon:
2020 self.tray_icon.set_visible(self.minimized)
2021 elif self.tray_icon:
2022 self.tray_icon.set_visible(True)
2024 def on_itemShowToolbar_activate(self, widget):
2025 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2027 def on_itemShowDescription_activate(self, widget):
2028 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2030 def update_item_device( self):
2031 if gl.config.device_type != 'none':
2032 self.itemDevice.show_all()
2033 (label,) = self.itemDevice.get_children()
2034 label.set_text(gl.get_device_name())
2035 else:
2036 self.itemDevice.hide_all()
2038 def properties_closed( self):
2039 self.show_hide_tray_icon()
2040 self.update_item_device()
2041 self.updateComboBox()
2043 def on_itemPreferences_activate(self, widget, *args):
2044 if gpodder.interface == gpodder.GUI:
2045 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2046 else:
2047 gPodderMaemoPreferences()
2049 def on_itemDependencies_activate(self, widget):
2050 gPodderDependencyManager()
2052 def on_add_new_google_search(self, widget, *args):
2053 def add_google_video_search(query):
2054 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2056 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2058 def on_itemAddChannel_activate(self, widget, *args):
2059 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2061 def on_itemEditChannel_activate(self, widget, *args):
2062 if self.active_channel is None:
2063 title = _('No podcast selected')
2064 message = _('Please select a podcast in the podcasts list to edit.')
2065 self.show_message( message, title)
2066 return
2068 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2070 def change_channel_url(self, old_url, new_url):
2071 channel = None
2072 try:
2073 channel = podcastChannel.load(url=new_url, create=True)
2074 except:
2075 channel = None
2077 if channel is None:
2078 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2079 return
2081 for channel in self.channels:
2082 if channel.url == old_url:
2083 log('=> change channel url from %s to %s', old_url, new_url)
2084 old_save_dir = channel.save_dir
2085 channel.url = new_url
2086 new_save_dir = channel.save_dir
2087 log('old save dir=%s', old_save_dir, sender=self)
2088 log('new save dir=%s', new_save_dir, sender=self)
2089 files = glob.glob(os.path.join(old_save_dir, '*'))
2090 log('moving %d files to %s', len(files), new_save_dir, sender=self)
2091 for file in files:
2092 log('moving %s', file, sender=self)
2093 shutil.move(file, new_save_dir)
2094 try:
2095 os.rmdir(old_save_dir)
2096 except:
2097 log('Warning: cannot delete %s', old_save_dir, sender=self)
2099 save_channels(self.channels)
2100 # update feed cache and select the podcast with the new URL afterwards
2101 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2103 def on_itemRemoveChannel_activate(self, widget, *args):
2104 try:
2105 if gpodder.interface == gpodder.GUI:
2106 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2107 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2108 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2110 title = _('Remove podcast and episodes?')
2111 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2113 dialog.set_title(title)
2114 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2116 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2117 dialog.vbox.pack_start(cb_ask)
2118 cb_ask.show_all()
2119 affirmative = gtk.RESPONSE_YES
2120 elif gpodder.interface == gpodder.MAEMO:
2121 cb_ask = gtk.CheckButton('') # dummy check button
2122 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2123 affirmative = gtk.RESPONSE_OK
2125 result = dialog.run()
2126 dialog.destroy()
2128 if result == affirmative:
2129 # delete downloaded episodes only if checkbox is unchecked
2130 if cb_ask.get_active() == False:
2131 self.active_channel.remove_downloaded()
2132 else:
2133 log('Not removing downloaded episodes', sender=self)
2135 # only delete partial files if we do not have any downloads in progress
2136 delete_partial = not services.download_status_manager.has_items()
2137 gl.clean_up_downloads(delete_partial)
2139 # cancel any active downloads from this channel
2140 if not delete_partial:
2141 for episode in self.active_channel.get_all_episodes():
2142 services.download_status_manager.cancel_by_url(episode.url)
2144 # get the URL of the podcast we want to select next
2145 position = self.channels.index(self.active_channel)
2146 if position == len(self.channels)-1:
2147 # this is the last podcast, so select the URL
2148 # of the item before this one (i.e. the "new last")
2149 select_url = self.channels[position-1].url
2150 else:
2151 # there is a podcast after the deleted one, so
2152 # we simply select the one that comes after it
2153 select_url = self.channels[position+1].url
2155 # Remove the channel
2156 self.active_channel.delete()
2157 self.channels.remove(self.active_channel)
2158 save_channels(self.channels)
2160 # Re-load the channels and select the desired new channel
2161 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2162 except:
2163 log('There has been an error removing the channel.', traceback=True, sender=self)
2164 self.update_podcasts_tab()
2166 def get_opml_filter(self):
2167 filter = gtk.FileFilter()
2168 filter.add_pattern('*.opml')
2169 filter.add_pattern('*.xml')
2170 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2171 return filter
2173 def on_item_import_from_file_activate(self, widget, filename=None):
2174 if filename is None:
2175 if gpodder.interface == gpodder.GUI:
2176 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2177 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2178 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2179 elif gpodder.interface == gpodder.MAEMO:
2180 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2181 dlg.set_filter(self.get_opml_filter())
2182 response = dlg.run()
2183 filename = None
2184 if response == gtk.RESPONSE_OK:
2185 filename = dlg.get_filename()
2186 dlg.destroy()
2188 if filename is not None:
2189 gPodderOpmlLister(custom_title=_('Import podcasts from OPML file'), hide_url_entry=True).get_channels_from_url(filename, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2191 def on_itemExportChannels_activate(self, widget, *args):
2192 if not self.channels:
2193 title = _('Nothing to export')
2194 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2195 self.show_message( message, title)
2196 return
2198 if gpodder.interface == gpodder.GUI:
2199 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2200 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2201 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2202 elif gpodder.interface == gpodder.MAEMO:
2203 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2204 dlg.set_filter(self.get_opml_filter())
2205 response = dlg.run()
2206 if response == gtk.RESPONSE_OK:
2207 filename = dlg.get_filename()
2208 dlg.destroy()
2209 exporter = opml.Exporter( filename)
2210 if exporter.write(self.channels):
2211 if len(self.channels) == 1:
2212 title = _('One subscription exported')
2213 else:
2214 title = _('%d subscriptions exported') % len(self.channels)
2215 self.show_message(_('Your podcast list has been successfully exported.'), title)
2216 else:
2217 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2218 else:
2219 dlg.destroy()
2221 def on_itemImportChannels_activate(self, widget, *args):
2222 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2224 def on_homepage_activate(self, widget, *args):
2225 util.open_website(app_website)
2227 def on_wiki_activate(self, widget, *args):
2228 util.open_website('http://wiki.gpodder.org/')
2230 def on_bug_tracker_activate(self, widget, *args):
2231 util.open_website('http://bugs.gpodder.org/')
2233 def on_itemAbout_activate(self, widget, *args):
2234 dlg = gtk.AboutDialog()
2235 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2236 dlg.set_version( app_version)
2237 dlg.set_copyright( app_copyright)
2238 dlg.set_website( app_website)
2239 dlg.set_translator_credits( _('translator-credits'))
2240 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2242 if gpodder.interface == gpodder.GUI:
2243 # For the "GUI" version, we add some more
2244 # items to the about dialog (credits and logo)
2245 dlg.set_authors(app_authors)
2246 try:
2247 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2248 except:
2249 pass
2251 dlg.run()
2253 def on_wNotebook_switch_page(self, widget, *args):
2254 page_num = args[1]
2255 if gpodder.interface == gpodder.MAEMO:
2256 page = self.wNotebook.get_nth_page(page_num)
2257 tab_label = self.wNotebook.get_tab_label(page).get_text()
2258 if page_num == 0 and self.active_channel is not None:
2259 self.set_title(self.active_channel.title)
2260 else:
2261 self.set_title(tab_label)
2262 if page_num == 0:
2263 self.play_or_download()
2264 else:
2265 self.toolDownload.set_sensitive( False)
2266 self.toolPlay.set_sensitive( False)
2267 self.toolTransfer.set_sensitive( False)
2268 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2270 def on_treeChannels_row_activated(self, widget, *args):
2271 self.on_itemEditChannel_activate( self.treeChannels)
2273 def on_treeChannels_cursor_changed(self, widget, *args):
2274 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2276 if model is not None and iter != None:
2277 id = model.get_path( iter)[0]
2278 self.active_channel = self.channels[id]
2280 if gpodder.interface == gpodder.MAEMO:
2281 self.set_title(self.active_channel.title)
2282 self.itemEditChannel.show_all()
2283 self.itemRemoveChannel.show_all()
2284 else:
2285 self.active_channel = None
2286 self.itemEditChannel.hide_all()
2287 self.itemRemoveChannel.hide_all()
2289 self.updateTreeView()
2291 def on_entryAddChannel_changed(self, widget, *args):
2292 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2293 self.btnAddChannel.set_sensitive( active)
2295 def on_btnAddChannel_clicked(self, widget, *args):
2296 url = self.entryAddChannel.get_text()
2297 self.entryAddChannel.set_text('')
2298 self.add_new_channel( url)
2300 def on_btnEditChannel_clicked(self, widget, *args):
2301 self.on_itemEditChannel_activate( widget, args)
2303 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2305 What this function does depends on from which widget it is called.
2306 It gets the selected episodes of the current podcast and runs one
2307 of the following actions on them:
2309 * Transfer (to MP3 player, iPod, etc..)
2310 * Playback/open files
2311 * Show the episode info dialog
2312 * Download episodes
2314 try:
2315 selection = self.treeAvailable.get_selection()
2316 (model, paths) = selection.get_selected_rows()
2318 wname = widget.get_name()
2319 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2320 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2321 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2323 episodes = []
2324 for path in paths:
2325 it = model.get_iter(path)
2326 url = model.get_value(it, 0)
2327 episode = self.active_channel.find_episode(url)
2328 episodes.append(episode)
2330 if len(episodes) == 0:
2331 log('No episodes selected', sender=self)
2333 if do_transfer:
2334 self.on_sync_to_ipod_activate(widget, episodes)
2335 elif do_playback:
2336 for episode in episodes:
2337 # Make sure to mark the episode as downloaded
2338 if os.path.exists(episode.local_filename()):
2339 episode.channel.addDownloadedItem(episode)
2340 self.playback_episode(episode)
2341 elif gl.config.enable_streaming:
2342 self.playback_episode(episode, stream=True)
2343 elif do_epdialog:
2344 play_callback = lambda: self.playback_episode(episode)
2345 download_callback = lambda: self.download_episode_list([episode])
2346 gPodderEpisode(episode=episode, download_callback=download_callback, play_callback=play_callback)
2347 else:
2348 self.download_episode_list(episodes)
2349 except:
2350 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2352 def on_treeAvailable_button_release_event(self, widget, *args):
2353 self.play_or_download()
2355 def auto_update_procedure(self, first_run=False):
2356 log('auto_update_procedure() got called', sender=self)
2357 if not first_run and gl.config.auto_update_feeds and self.minimized:
2358 self.update_feed_cache(force_update=True)
2360 next_update = 60*1000*gl.config.auto_update_frequency
2361 gobject.timeout_add(next_update, self.auto_update_procedure)
2363 def on_treeDownloads_row_activated(self, widget, *args):
2364 cancel_urls = []
2366 if self.wNotebook.get_current_page() > 0:
2367 # Use the download list treeview + model
2368 ( tree, column ) = ( self.treeDownloads, 3 )
2369 else:
2370 # Use the available podcasts treeview + model
2371 ( tree, column ) = ( self.treeAvailable, 0 )
2373 selection = tree.get_selection()
2374 (model, paths) = selection.get_selected_rows()
2375 for path in paths:
2376 url = model.get_value( model.get_iter( path), column)
2377 cancel_urls.append( url)
2379 if len( cancel_urls) == 0:
2380 log('Nothing selected.', sender = self)
2381 return
2383 if len( cancel_urls) == 1:
2384 title = _('Cancel download?')
2385 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2386 else:
2387 title = _('Cancel downloads?')
2388 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2390 if self.show_confirmation( message, title):
2391 services.download_status_manager.start_batch_mode()
2392 for url in cancel_urls:
2393 services.download_status_manager.cancel_by_url( url)
2394 services.download_status_manager.end_batch_mode()
2396 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2397 self.on_treeDownloads_row_activated( widget, None)
2399 def on_btnCancelAll_clicked(self, widget, *args):
2400 self.treeDownloads.get_selection().select_all()
2401 self.on_treeDownloads_row_activated( self.toolCancel, None)
2402 self.treeDownloads.get_selection().unselect_all()
2404 def on_btnDownloadedDelete_clicked(self, widget, *args):
2405 if self.active_channel is None:
2406 return
2408 channel_url = self.active_channel.url
2409 selection = self.treeAvailable.get_selection()
2410 ( model, paths ) = selection.get_selected_rows()
2412 if selection.count_selected_rows() == 0:
2413 log( 'Nothing selected - will not remove any downloaded episode.')
2414 return
2416 if selection.count_selected_rows() == 1:
2417 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2419 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2420 if episode['is_locked']:
2421 title = _('%s is locked') % episode_title
2422 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2423 self.notification(message, title)
2424 return
2426 title = _('Remove %s?') % episode_title
2427 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.")
2428 else:
2429 title = _('Remove %d episodes?') % selection.count_selected_rows()
2430 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.')
2432 locked_count = 0
2433 for path in paths:
2434 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2435 if episode['is_locked']:
2436 locked_count += 1
2438 if selection.count_selected_rows() == locked_count:
2439 title = _('Episodes are locked')
2440 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2441 self.notification(message, title)
2442 return
2443 elif locked_count > 0:
2444 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2445 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.')
2447 # if user confirms deletion, let's remove some stuff ;)
2448 if self.show_confirmation( message, title):
2449 try:
2450 # iterate over the selection, see also on_treeDownloads_row_activated
2451 for path in paths:
2452 url = model.get_value( model.get_iter( path), 0)
2453 self.active_channel.delete_episode_by_url( url)
2455 # now, clear local db cache so we can re-read it
2456 self.updateComboBox()
2457 except:
2458 log( 'Error while deleting (some) downloads.')
2460 # only delete partial files if we do not have any downloads in progress
2461 delete_partial = not services.download_status_manager.has_items()
2462 gl.clean_up_downloads(delete_partial)
2463 self.updateTreeView()
2465 def on_key_press(self, widget, event):
2466 # Allow tab switching with Ctrl + PgUp/PgDown
2467 if event.state & gtk.gdk.CONTROL_MASK:
2468 if event.keyval == gtk.keysyms.Page_Up:
2469 self.wNotebook.prev_page()
2470 return True
2471 elif event.keyval == gtk.keysyms.Page_Down:
2472 self.wNotebook.next_page()
2473 return True
2475 # After this code we only handle Maemo hardware keys,
2476 # so if we are not a Maemo app, we don't do anything
2477 if gpodder.interface != gpodder.MAEMO:
2478 return False
2480 if event.keyval == gtk.keysyms.F6:
2481 if self.fullscreen:
2482 self.window.unfullscreen()
2483 else:
2484 self.window.fullscreen()
2485 if event.keyval == gtk.keysyms.Escape:
2486 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2487 self.vboxChannelNavigator.set_property('visible', new_visibility)
2488 self.column_size.set_visible(not new_visibility)
2489 self.column_released.set_visible(not new_visibility)
2491 diff = 0
2492 if event.keyval == gtk.keysyms.F7: #plus
2493 diff = 1
2494 elif event.keyval == gtk.keysyms.F8: #minus
2495 diff = -1
2497 if diff != 0:
2498 selection = self.treeChannels.get_selection()
2499 (model, iter) = selection.get_selected()
2500 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2501 self.on_treeChannels_cursor_changed(self.treeChannels)
2503 def window_state_event(self, widget, event):
2504 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2505 self.fullscreen = True
2506 else:
2507 self.fullscreen = False
2509 old_minimized = self.minimized
2511 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2512 if gpodder.interface == gpodder.MAEMO:
2513 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2515 if old_minimized != self.minimized and self.tray_icon:
2516 self.gPodder.set_skip_taskbar_hint(self.minimized)
2517 elif not self.tray_icon:
2518 self.gPodder.set_skip_taskbar_hint(False)
2520 if gl.config.minimize_to_tray and self.tray_icon:
2521 self.tray_icon.set_visible(self.minimized)
2523 def uniconify_main_window(self):
2524 if self.minimized:
2525 self.gPodder.present()
2527 def iconify_main_window(self):
2528 if not self.minimized:
2529 self.gPodder.iconify()
2531 def update_podcasts_tab(self):
2532 if len(self.channels):
2533 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2534 else:
2535 self.label2.set_text(_('Podcasts'))
2537 class gPodderChannel(GladeWidget):
2538 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description']
2540 def new(self):
2541 global WEB_BROWSER_ICON
2542 self.changed = False
2543 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2544 self.gPodderChannel.set_title( self.channel.title)
2545 self.entryTitle.set_text( self.channel.title)
2546 self.entryURL.set_text( self.channel.url)
2548 self.LabelDownloadTo.set_text( self.channel.save_dir)
2549 self.LabelWebsite.set_text( self.channel.link)
2551 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2552 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2553 if self.channel.username:
2554 self.FeedUsername.set_text( self.channel.username)
2555 if self.channel.password:
2556 self.FeedPassword.set_text( self.channel.password)
2558 services.cover_downloader.register('cover-available', self.cover_download_finished)
2559 services.cover_downloader.request_cover(self.channel)
2561 # Hide the website button if we don't have a valid URL
2562 if not self.channel.link:
2563 self.btn_website.hide_all()
2565 b = gtk.TextBuffer()
2566 b.set_text( self.channel.description)
2567 self.channel_description.set_buffer( b)
2569 #Add Drag and Drop Support
2570 flags = gtk.DEST_DEFAULT_ALL
2571 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2572 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2573 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2574 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2576 def on_btn_website_clicked(self, widget):
2577 util.open_website(self.channel.link)
2579 def on_btnDownloadCover_clicked(self, widget):
2580 if gpodder.interface == gpodder.GUI:
2581 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2582 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2583 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2584 elif gpodder.interface == gpodder.MAEMO:
2585 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2587 if dlg.run() == gtk.RESPONSE_OK:
2588 url = dlg.get_uri()
2589 services.cover_downloader.replace_cover(self.channel, url)
2591 dlg.destroy()
2593 def on_btnClearCover_clicked(self, widget):
2594 services.cover_downloader.replace_cover(self.channel)
2596 def cover_download_finished(self, channel_url, pixbuf):
2597 if pixbuf is not None:
2598 self.imgCover.set_from_pixbuf(pixbuf)
2599 self.gPodderChannel.show()
2601 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2602 files = sel.data.strip().split('\n')
2603 if len(files) != 1:
2604 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2605 return
2607 file = files[0]
2609 if file.startswith('file://') or file.startswith('http://'):
2610 services.cover_downloader.replace_cover(self.channel, file)
2611 return
2613 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2615 def on_gPodderChannel_destroy(self, widget, *args):
2616 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2618 def on_btnOK_clicked(self, widget, *args):
2619 entered_url = self.entryURL.get_text()
2620 channel_url = self.channel.url
2622 if entered_url != channel_url:
2623 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2624 if hasattr(self, 'callback_change_url'):
2625 self.gPodderChannel.hide_all()
2626 self.callback_change_url(channel_url, entered_url)
2628 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2629 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2630 self.channel.set_custom_title( self.entryTitle.get_text())
2631 self.channel.username = self.FeedUsername.get_text().strip()
2632 self.channel.password = self.FeedPassword.get_text()
2633 self.channel.save()
2635 self.gPodderChannel.destroy()
2636 self.callback_closed()
2638 class gPodderAddPodcastDialog(GladeWidget):
2639 finger_friendly_widgets = ['btn_close', 'btn_add']
2641 def new(self):
2642 if not hasattr(self, 'url_callback'):
2643 log('No url callback set', sender=self)
2644 self.url_callback = None
2645 if hasattr(self, 'custom_label'):
2646 self.label_add.set_text(self.custom_label)
2647 if hasattr(self, 'custom_title'):
2648 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2650 def on_btn_close_clicked(self, widget):
2651 self.gPodderAddPodcastDialog.destroy()
2653 def on_entry_url_changed(self, widget):
2654 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2656 def on_btn_add_clicked(self, widget):
2657 url = self.entry_url.get_text()
2658 self.on_btn_close_clicked(widget)
2659 if self.url_callback is not None:
2660 self.url_callback(url)
2663 class gPodderMaemoPreferences(GladeWidget):
2664 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2666 def new(self):
2667 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2668 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2669 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2670 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2672 self.restart_required = False
2673 self.show_tray_icon.connect('clicked', self.on_restart_required)
2674 self.show_notifications.connect('clicked', self.on_restart_required)
2676 def on_restart_required(self, widget):
2677 self.restart_required = True
2679 def on_btn_advanced_clicked(self, widget):
2680 self.gPodderMaemoPreferences.destroy()
2681 gPodderConfigEditor()
2683 def on_btn_close_clicked(self, widget):
2684 self.gPodderMaemoPreferences.destroy()
2685 if self.restart_required:
2686 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2689 class gPodderProperties(GladeWidget):
2690 def new(self):
2691 if not hasattr( self, 'callback_finished'):
2692 self.callback_finished = None
2694 if gpodder.interface == gpodder.MAEMO:
2695 self.table13.hide_all() # bluetooth
2696 self.table5.hide_all() # player
2697 self.gPodderProperties.fullscreen()
2699 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2700 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2701 gl.config.connect_gtk_editable( 'player', self.openApp)
2702 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2703 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2704 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2705 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2706 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2707 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2708 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2709 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2710 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2711 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2712 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2713 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2714 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2715 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2716 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2717 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2718 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2719 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2720 gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
2721 gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
2722 gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
2723 gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
2724 gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
2725 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2726 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2728 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2729 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2731 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2733 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2734 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2735 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2736 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2738 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2739 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2741 if tagging_supported():
2742 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
2743 else:
2744 self.updatetags.set_sensitive( False)
2745 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2746 self.updatetags.set_label( new_label)
2748 # device type
2749 self.comboboxDeviceType.set_active( 0)
2750 if gl.config.device_type == 'ipod':
2751 self.comboboxDeviceType.set_active( 1)
2752 elif gl.config.device_type == 'filesystem':
2753 self.comboboxDeviceType.set_active( 2)
2754 elif gl.config.device_type == 'mtp':
2755 self.comboboxDeviceType.set_active( 3)
2757 # setup cell renderers
2758 cellrenderer = gtk.CellRendererPixbuf()
2759 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2760 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2761 cellrenderer = gtk.CellRendererText()
2762 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2763 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2765 cellrenderer = gtk.CellRendererPixbuf()
2766 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2767 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2768 cellrenderer = gtk.CellRendererText()
2769 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2770 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2772 if not hasattr(self, 'user_apps_reader'):
2773 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2775 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
2776 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
2778 if gpodder.interface == gpodder.GUI:
2779 self.user_apps_reader.read()
2781 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2782 index = self.find_active_audio_app()
2783 self.comboAudioPlayerApp.set_active(index)
2784 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2785 index = self.find_active_video_app()
2786 self.comboVideoPlayerApp.set_active(index)
2788 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2790 def is_row_separator(self, model, iter):
2791 return model.get_value(iter, 0) == ''
2793 def update_mountpoint( self, ipod):
2794 if ipod is None or ipod.mount_point is None:
2795 self.iPodMountpoint.set_label( '')
2796 else:
2797 self.iPodMountpoint.set_label( ipod.mount_point)
2799 def on_bluetooth_select_device_clicked(self, widget):
2800 # Stupid GTK doesn't provide us with a method to directly
2801 # edit the text of a gtk.Button without "destroying" the
2802 # image on it, so we dig into the button's widget tree and
2803 # get the gtk.Image and gtk.Label and edit the label directly.
2804 alignment = self.bluetooth_select_device.get_child()
2805 hbox = alignment.get_child()
2806 (image, label) = hbox.get_children()
2808 old_text = label.get_text()
2809 label.set_text(_('Searching...'))
2810 self.bluetooth_select_device.set_sensitive(False)
2811 while gtk.events_pending():
2812 gtk.main_iteration(False)
2814 # FIXME: Make bluetooth device discovery threaded, so
2815 # the GUI doesn't freeze while we are searching for devices
2816 found = False
2817 for name, address in util.discover_bluetooth_devices():
2818 if self.show_confirmation('Use this device as your bluetooth device?', name):
2819 gl.config.bluetooth_device_name = name
2820 gl.config.bluetooth_device_address = address
2821 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2822 found = True
2823 break
2824 if not found:
2825 self.show_message('No more devices found', 'Scan finished')
2826 self.bluetooth_select_device.set_sensitive(True)
2827 label.set_text(old_text)
2829 def find_active_audio_app(self):
2830 model = self.comboAudioPlayerApp.get_model()
2831 iter = model.get_iter_first()
2832 index = 0
2833 while iter is not None:
2834 command = model.get_value(iter, 1)
2835 if command == self.openApp.get_text():
2836 return index
2837 iter = model.iter_next(iter)
2838 index += 1
2839 # return last item = custom command
2840 return index-1
2842 def find_active_video_app( self):
2843 model = self.comboVideoPlayerApp.get_model()
2844 iter = model.get_iter_first()
2845 index = 0
2846 while iter is not None:
2847 command = model.get_value(iter, 1)
2848 if command == self.openVideoApp.get_text():
2849 return index
2850 iter = model.iter_next(iter)
2851 index += 1
2852 # return last item = custom command
2853 return index-1
2855 def set_download_dir( self, new_download_dir, event = None):
2856 gl.downloaddir = self.chooserDownloadTo.get_filename()
2857 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2858 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'))
2860 if event:
2861 event.set()
2863 def on_auto_update_feeds_toggled( self, widget, *args):
2864 self.auto_update_frequency.set_sensitive(widget.get_active())
2866 def on_display_tray_icon_toggled( self, widget, *args):
2867 self.enable_notifications.set_sensitive(widget.get_active())
2868 self.minimize_to_tray.set_sensitive(widget.get_active())
2870 def on_cbCustomSyncName_toggled( self, widget, *args):
2871 self.entryCustomSyncName.set_sensitive( widget.get_active())
2873 def on_only_sync_not_played_toggled( self, widget, *args):
2874 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
2875 if not widget.get_active():
2876 self.delete_episodes_marked_played.set_active(False)
2878 def on_delete_episodes_marked_played_toggled( self, widget, *args):
2879 if widget.get_active() and self.only_sync_not_played.get_active():
2880 self.on_sync_leave.set_active(True)
2881 self.on_sync_delete.set_sensitive(not widget.get_active())
2882 self.on_sync_mark_played.set_sensitive(not widget.get_active())
2884 def on_btnCustomSyncNameHelp_clicked( self, widget):
2885 examples = [
2886 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2887 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2888 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
2891 info = [
2892 _('You can specify a custom format string for the file names on your MP3 player here.'),
2893 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2894 '\n'.join( [ ' %s' % s for s in examples ])
2897 self.show_message( '\n\n'.join( info), _('Custom format strings'))
2899 def on_gPodderProperties_destroy(self, widget, *args):
2900 self.on_btnOK_clicked( widget, *args)
2902 def on_btnConfigEditor_clicked(self, widget, *args):
2903 self.on_btnOK_clicked(widget, *args)
2904 gPodderConfigEditor()
2906 def on_comboAudioPlayerApp_changed(self, widget, *args):
2907 # find out which one
2908 iter = self.comboAudioPlayerApp.get_active_iter()
2909 model = self.comboAudioPlayerApp.get_model()
2910 command = model.get_value( iter, 1)
2911 if command == '':
2912 if self.openApp.get_text() == 'default':
2913 self.openApp.set_text('')
2914 self.openApp.set_sensitive( True)
2915 self.openApp.show()
2916 self.labelCustomCommand.show()
2917 else:
2918 self.openApp.set_text( command)
2919 self.openApp.set_sensitive( False)
2920 self.openApp.hide()
2921 self.labelCustomCommand.hide()
2923 def on_comboVideoPlayerApp_changed(self, widget, *args):
2924 # find out which one
2925 iter = self.comboVideoPlayerApp.get_active_iter()
2926 model = self.comboVideoPlayerApp.get_model()
2927 command = model.get_value(iter, 1)
2928 if command == '':
2929 if self.openVideoApp.get_text() == 'default':
2930 self.openVideoApp.set_text('')
2931 self.openVideoApp.set_sensitive(True)
2932 self.openVideoApp.show()
2933 self.labelCustomVideoCommand.show()
2934 else:
2935 self.openVideoApp.set_text(command)
2936 self.openVideoApp.set_sensitive(False)
2937 self.openVideoApp.hide()
2938 self.labelCustomVideoCommand.hide()
2940 def on_cbEnvironmentVariables_toggled(self, widget, *args):
2941 sens = not self.cbEnvironmentVariables.get_active()
2942 self.httpProxy.set_sensitive( sens)
2943 self.ftpProxy.set_sensitive( sens)
2945 def on_comboboxDeviceType_changed(self, widget, *args):
2946 active_item = self.comboboxDeviceType.get_active()
2948 # None
2949 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
2950 self.imageSyncOptions, self. separatorSyncOptions,
2951 self.on_sync_mark_played, self.on_sync_delete,
2952 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
2953 for widget in sync_widgets:
2954 if active_item == 0:
2955 widget.hide_all()
2956 else:
2957 widget.show_all()
2959 # iPod
2960 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
2961 self.ipod_write_gtkpod_extended)
2962 for widget in ipod_widgets:
2963 if active_item == 1:
2964 widget.show_all()
2965 else:
2966 widget.hide_all()
2968 # filesystem-based MP3 player
2969 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
2970 self.cbChannelSubfolder, self.cbCustomSyncName,
2971 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
2972 for widget in fs_widgets:
2973 if active_item == 2:
2974 widget.show_all()
2975 else:
2976 widget.hide_all()
2978 def on_btn_iPodMountpoint_clicked(self, widget, *args):
2979 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2980 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2981 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2982 fs.set_current_folder(self.iPodMountpoint.get_label())
2983 if fs.run() == gtk.RESPONSE_OK:
2984 self.iPodMountpoint.set_label( fs.get_filename())
2985 fs.destroy()
2987 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
2988 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2989 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2990 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2991 fs.set_current_folder(self.filesystemMountpoint.get_label())
2992 if fs.run() == gtk.RESPONSE_OK:
2993 self.filesystemMountpoint.set_label( fs.get_filename())
2994 fs.destroy()
2996 def on_btnOK_clicked(self, widget, *args):
2997 gl.config.ipod_mount = self.iPodMountpoint.get_label()
2998 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3000 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3001 new_download_dir = self.chooserDownloadTo.get_filename()
3002 download_dir_size = util.calculate_size( gl.downloaddir)
3003 download_dir_size_string = gl.format_filesize( download_dir_size)
3004 event = Event()
3006 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3007 dlg.vbox.set_spacing( 5)
3008 dlg.set_border_width( 5)
3010 label = gtk.Label()
3011 label.set_line_wrap( True)
3012 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3013 myprogressbar = gtk.ProgressBar()
3015 # put it all together
3016 dlg.vbox.pack_start( label)
3017 dlg.vbox.pack_end( myprogressbar)
3019 # switch windows
3020 dlg.show_all()
3021 self.gPodderProperties.hide_all()
3023 # hide action area and separator line
3024 dlg.action_area.hide()
3025 dlg.set_has_separator( False)
3027 args = ( new_download_dir, event, )
3029 thread = Thread( target = self.set_download_dir, args = args)
3030 thread.start()
3032 while not event.isSet():
3033 try:
3034 new_download_dir_size = util.calculate_size( new_download_dir)
3035 except:
3036 new_download_dir_size = 0
3037 if download_dir_size > 0:
3038 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3039 else:
3040 fract = 0.0
3041 if fract < 0.99:
3042 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3043 else:
3044 myprogressbar.set_text( _('Finishing... please wait.'))
3045 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3046 event.wait( 0.1)
3047 while gtk.events_pending():
3048 gtk.main_iteration( False)
3050 dlg.destroy()
3052 device_type = self.comboboxDeviceType.get_active()
3053 if device_type == 0:
3054 gl.config.device_type = 'none'
3055 elif device_type == 1:
3056 gl.config.device_type = 'ipod'
3057 elif device_type == 2:
3058 gl.config.device_type = 'filesystem'
3059 elif device_type == 3:
3060 gl.config.device_type = 'mtp'
3061 self.gPodderProperties.destroy()
3062 if self.callback_finished:
3063 self.callback_finished()
3066 class gPodderEpisode(GladeWidget):
3067 finger_friendly_widgets = ['episode_description', 'btnCloseWindow', 'btnDownload',
3068 'btnCancel', 'btnPlay', 'btn_website']
3070 def new(self):
3071 global WEB_BROWSER_ICON
3072 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
3073 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
3074 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
3076 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
3078 if gpodder.interface == gpodder.MAEMO:
3079 # Hide the advanced prefs expander
3080 self.expander1.hide_all()
3082 try:
3083 import gtkhtml2
3084 document = gtkhtml2.Document()
3085 document.connect('link-clicked', lambda d, url: util.open_website(url))
3086 def request_url(document, url, stream):
3087 stream.write(urllib2.urlopen(url).read())
3088 stream.close()
3089 document.connect('request-url', request_url)
3090 document.clear()
3091 document.open_stream('text/html')
3092 document.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3093 document.write_stream('<h2>%s</h2><small><em>from <a href="%s">%s</a>, released %s</em></small><br><hr style="border: 1px #eeeeee solid;">' % (saxutils.escape(self.episode.title), self.episode.link, saxutils.escape(self.episode.channel.title), self.episode.cute_pubdate()))
3094 document.write_stream(self.episode.description)
3095 document.write_stream('<br><hr style="border: 1px #eeeeee solid;"><p style="font-size: 8px;">%s</p>' % self.episode.link)
3096 document.write_stream('</body></html>')
3097 document.close_stream()
3098 self.gPodderEpisode.resize(500, 500)
3100 self.episode_title.hide_all()
3101 self.channel_title.hide_all()
3102 self.btn_website.hide_all()
3103 self.expander1.hide_all()
3105 view = gtkhtml2.View()
3106 view.set_document(document)
3107 self.scrolledwindow4.remove(self.scrolledwindow4.get_child())
3108 self.scrolledwindow4.add(view)
3109 view.show()
3110 except ImportError, ie:
3111 b = gtk.TextBuffer()
3112 b.set_text(strip(util.remove_html_tags(self.episode.description)))
3113 self.episode_description.set_buffer( b)
3115 self.gPodderEpisode.set_title( self.episode.title)
3116 self.LabelDownloadLink.set_text( self.episode.url)
3117 self.LabelWebsiteLink.set_text( self.episode.link)
3118 self.labelPubDate.set_text(self.episode.cute_pubdate())
3120 # Hide the "Go to website" button if we don't have a valid URL
3121 if self.episode.link == self.episode.url or not self.episode.link:
3122 self.btn_website.hide_all()
3124 self.channel_title.set_markup(_('<i>from %s</i>') % saxutils.escape(self.episode.channel.title))
3126 self.hide_show_widgets()
3127 services.download_status_manager.request_progress_detail( self.episode.url)
3129 def on_btnCancel_clicked( self, widget):
3130 services.download_status_manager.cancel_by_url( self.episode.url)
3132 def on_gPodderEpisode_destroy( self, widget):
3133 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
3134 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
3136 def on_download_status_changed( self):
3137 self.hide_show_widgets()
3139 def on_btn_website_clicked(self, widget):
3140 util.open_website(self.episode.link)
3142 def on_download_status_progress( self, url, progress, speed):
3143 if url == self.episode.url:
3144 progress = float(min(100.0,max(0.0,progress)))
3145 self.progress_bar.set_fraction(progress/100.0)
3146 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
3148 def hide_show_widgets( self):
3149 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
3150 if is_downloading:
3151 self.progress_bar.show_all()
3152 self.btnCancel.show_all()
3153 self.btnPlay.hide_all()
3154 self.btnDownload.hide_all()
3155 else:
3156 self.progress_bar.hide_all()
3157 self.btnCancel.hide_all()
3158 if os.path.exists( self.episode.local_filename()):
3159 if self.episode.file_type() in ('audio', 'video'):
3160 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3161 else:
3162 self.btnPlay.set_label(gtk.STOCK_OPEN)
3163 self.btnPlay.set_use_stock(True)
3164 self.btnPlay.show_all()
3165 self.btnDownload.hide_all()
3166 else:
3167 self.btnPlay.hide_all()
3168 self.btnDownload.show_all()
3170 def on_btnCloseWindow_clicked(self, widget, *args):
3171 self.gPodderEpisode.destroy()
3173 def on_btnDownload_clicked(self, widget, *args):
3174 if self.download_callback:
3175 self.download_callback()
3177 def on_btnPlay_clicked(self, widget, *args):
3178 if self.play_callback:
3179 self.play_callback()
3181 self.gPodderEpisode.destroy()
3184 class gPodderSync(GladeWidget):
3185 def new(self):
3186 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3188 self.device.register('progress', self.on_progress)
3189 self.device.register('sub-progress', self.on_sub_progress)
3190 self.device.register('status', self.on_status)
3191 self.device.register('done', self.on_done)
3193 def on_progress(self, pos, max, text=None):
3194 if text is None:
3195 text = _('%d of %d done') % (pos, max)
3196 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3197 util.idle_add(self.progressbar.set_text, text)
3199 def on_sub_progress(self, percentage):
3200 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3202 def on_status(self, status):
3203 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3205 def on_done(self):
3206 util.idle_add(self.gPodderSync.destroy)
3207 if not self.gPodder.minimized:
3208 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3210 def on_gPodderSync_destroy(self, widget, *args):
3211 self.device.unregister('progress', self.on_progress)
3212 self.device.unregister('sub-progress', self.on_sub_progress)
3213 self.device.unregister('status', self.on_status)
3214 self.device.unregister('done', self.on_done)
3215 self.device.cancel()
3217 def on_cancel_button_clicked(self, widget, *args):
3218 self.device.cancel()
3221 class gPodderOpmlLister(GladeWidget):
3222 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3224 def new(self):
3225 # initiate channels list
3226 self.channels = []
3227 self.callback_for_channel = None
3228 self.callback_finished = None
3230 if hasattr(self, 'custom_title'):
3231 self.gPodderOpmlLister.set_title(self.custom_title)
3232 if hasattr(self, 'hide_url_entry'):
3233 self.hbox25.hide_all()
3235 togglecell = gtk.CellRendererToggle()
3236 togglecell.set_property( 'activatable', True)
3237 togglecell.connect( 'toggled', self.callback_edited)
3238 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3240 titlecell = gtk.CellRendererText()
3241 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3242 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3244 for itemcolumn in ( togglecolumn, titlecolumn ):
3245 self.treeviewChannelChooser.append_column( itemcolumn)
3247 def callback_edited( self, cell, path):
3248 model = self.treeviewChannelChooser.get_model()
3250 url = model[path][2]
3252 model[path][0] = not model[path][0]
3253 if model[path][0]:
3254 self.channels.append( url)
3255 else:
3256 self.channels.remove( url)
3258 self.btnOK.set_sensitive( bool(len(self.channels)))
3260 def thread_finished(self, model):
3261 self.treeviewChannelChooser.set_model(model)
3262 self.btnDownloadOpml.set_sensitive(True)
3263 self.entryURL.set_sensitive(True)
3264 self.treeviewChannelChooser.set_sensitive(True)
3265 self.channels = []
3267 def thread_func(self):
3268 url = self.entryURL.get_text()
3269 importer = opml.Importer(url)
3270 model = importer.get_model()
3271 if len(model) == 0:
3272 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3273 util.idle_add(self.thread_finished, model)
3275 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3276 if callback_for_channel:
3277 self.callback_for_channel = callback_for_channel
3278 if callback_finished:
3279 self.callback_finished = callback_finished
3280 self.entryURL.set_text( url)
3281 self.btnDownloadOpml.set_sensitive( False)
3282 self.entryURL.set_sensitive( False)
3283 self.btnOK.set_sensitive( False)
3284 self.treeviewChannelChooser.set_sensitive( False)
3285 Thread( target = self.thread_func).start()
3287 def select_all( self, value ):
3288 self.channels = []
3289 for row in self.treeviewChannelChooser.get_model():
3290 row[0] = value
3291 if value:
3292 self.channels.append(row[2])
3293 self.btnOK.set_sensitive(bool(len(self.channels)))
3295 def on_gPodderOpmlLister_destroy(self, widget, *args):
3296 pass
3298 def on_btnDownloadOpml_clicked(self, widget, *args):
3299 self.get_channels_from_url( self.entryURL.get_text())
3301 def on_btnSelectAll_clicked(self, widget, *args):
3302 self.select_all(True)
3304 def on_btnSelectNone_clicked(self, widget, *args):
3305 self.select_all(False)
3307 def on_btnOK_clicked(self, widget, *args):
3308 self.gPodderOpmlLister.destroy()
3310 # add channels that have been selected
3311 for url in self.channels:
3312 if self.callback_for_channel:
3313 self.callback_for_channel( url)
3315 if self.callback_finished:
3316 util.idle_add(self.callback_finished)
3318 def on_btnCancel_clicked(self, widget, *args):
3319 self.gPodderOpmlLister.destroy()
3322 class gPodderEpisodeSelector( GladeWidget):
3323 """Episode selection dialog
3325 Optional keyword arguments that modify the behaviour of this dialog:
3327 - callback: Function that takes 1 parameter which is a list of
3328 the selected episodes (or empty list when none selected)
3329 - remove_callback: Function that takes 1 parameter which is a list
3330 of episodes that should be "removed" (see below)
3331 (default is None, which means remove not possible)
3332 - remove_action: Label for the "remove" action (default is "Remove")
3333 - remove_finished: Callback after all remove callbacks have finished
3334 (default is None, also depends on remove_callback)
3335 - episodes: List of episodes that are presented for selection
3336 - selected: (optional) List of boolean variables that define the
3337 default checked state for the given episodes
3338 - selected_default: (optional) The default boolean value for the
3339 checked state if no other value is set
3340 (default is False)
3341 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3342 columns, the name is the attribute name of the episode to be
3343 read from each episode object. The sort name is the
3344 attribute name of the episode to be used to sort this column.
3345 If the sort_name is None it will use the attribute name for
3346 sorting. The sort type is the type of the sort column.
3347 The caption attribute is the text that appear as column caption
3348 (default is [('title_and_description', None, None, 'Episode'),])
3349 - title: (optional) The title of the window + heading
3350 - instructions: (optional) A one-line text describing what the
3351 user should select / what the selection is for
3352 - stock_ok_button: (optional) Will replace the "OK" button with
3353 another GTK+ stock item to be used for the
3354 affirmative button of the dialog (e.g. can
3355 be gtk.STOCK_DELETE when the episodes to be
3356 selected will be deleted after closing the
3357 dialog)
3358 - selection_buttons: (optional) A dictionary with labels as
3359 keys and callbacks as values; for each
3360 key a button will be generated, and when
3361 the button is clicked, the callback will
3362 be called for each episode and the return
3363 value of the callback (True or False) will
3364 be the new selected state of the episode
3365 - size_attribute: (optional) The name of an attribute of the
3366 supplied episode objects that can be used to
3367 calculate the size of an episode; set this to
3368 None if no total size calculation should be
3369 done (in cases where total size is useless)
3370 (default is 'length')
3371 - tooltip_attribute: (optional) The name of an attribute of
3372 the supplied episode objects that holds
3373 the text for the tooltips when hovering
3374 over an episode (default is 'description')
3377 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3379 COLUMN_INDEX = 0
3380 COLUMN_TOOLTIP = 1
3381 COLUMN_TOGGLE = 2
3382 COLUMN_ADDITIONAL = 3
3384 def new( self):
3385 if not hasattr( self, 'callback'):
3386 self.callback = None
3388 if not hasattr(self, 'remove_callback'):
3389 self.remove_callback = None
3391 if not hasattr(self, 'remove_action'):
3392 self.remove_action = _('Remove')
3394 if not hasattr(self, 'remove_finished'):
3395 self.remove_finished = None
3397 if not hasattr( self, 'episodes'):
3398 self.episodes = []
3400 if not hasattr( self, 'size_attribute'):
3401 self.size_attribute = 'length'
3403 if not hasattr(self, 'tooltip_attribute'):
3404 self.tooltip_attribute = 'description'
3406 if not hasattr( self, 'selection_buttons'):
3407 self.selection_buttons = {}
3409 if not hasattr( self, 'selected_default'):
3410 self.selected_default = False
3412 if not hasattr( self, 'selected'):
3413 self.selected = [self.selected_default]*len(self.episodes)
3415 if len(self.selected) < len(self.episodes):
3416 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3418 if not hasattr( self, 'columns'):
3419 self.columns = (('title_and_description', None, None, _('Episode')),)
3421 if hasattr( self, 'title'):
3422 self.gPodderEpisodeSelector.set_title( self.title)
3423 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3425 if gpodder.interface == gpodder.MAEMO:
3426 self.labelHeading.hide()
3428 if hasattr( self, 'instructions'):
3429 self.labelInstructions.set_text( self.instructions)
3430 self.labelInstructions.show_all()
3432 if hasattr(self, 'stock_ok_button'):
3433 if self.stock_ok_button == 'gpodder-download':
3434 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3435 self.btnOK.set_label(_('Download'))
3436 else:
3437 self.btnOK.set_label(self.stock_ok_button)
3438 self.btnOK.set_use_stock(True)
3440 # check/uncheck column
3441 toggle_cell = gtk.CellRendererToggle()
3442 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3443 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3445 next_column = self.COLUMN_ADDITIONAL
3446 for name, sort_name, sort_type, caption in self.columns:
3447 renderer = gtk.CellRendererText()
3448 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3449 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3450 column.set_resizable( True)
3451 # Only set "expand" on the first column (so more text is displayed there)
3452 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3453 if sort_name is not None:
3454 column.set_sort_column_id(next_column+1)
3455 else:
3456 column.set_sort_column_id(next_column)
3457 self.treeviewEpisodes.append_column( column)
3458 next_column += 1
3460 if sort_name is not None:
3461 # add the sort column
3462 column = gtk.TreeViewColumn()
3463 column.set_visible(False)
3464 self.treeviewEpisodes.append_column( column)
3465 next_column += 1
3467 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3468 # add string column type plus sort column type if it exists
3469 for name, sort_name, sort_type, caption in self.columns:
3470 column_types.append(gobject.TYPE_STRING)
3471 if sort_name is not None:
3472 column_types.append(sort_type)
3473 self.model = gtk.ListStore( *column_types)
3475 tooltip = None
3476 for index, episode in enumerate( self.episodes):
3477 if self.tooltip_attribute is not None:
3478 try:
3479 tooltip = getattr(episode, self.tooltip_attribute)
3480 except:
3481 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3482 tooltip = None
3483 row = [ index, tooltip, self.selected[index] ]
3484 for name, sort_name, sort_type, caption in self.columns:
3485 if not hasattr(episode, name):
3486 log('Warning: Missing attribute "%s"', name, sender=self)
3487 row.append(None)
3488 else:
3489 row.append(getattr( episode, name))
3491 if sort_name is not None:
3492 if not hasattr(episode, sort_name):
3493 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3494 row.append(None)
3495 else:
3496 row.append(getattr( episode, sort_name))
3497 self.model.append( row)
3499 if self.remove_callback is not None:
3500 self.btnRemoveAction.show()
3501 self.btnRemoveAction.set_label(self.remove_action)
3503 # connect to tooltip signals
3504 if self.tooltip_attribute is not None:
3505 try:
3506 self.treeviewEpisodes.set_property('has-tooltip', True)
3507 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3508 except:
3509 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3510 self.last_tooltip_episode = None
3511 self.episode_list_can_tooltip = True
3513 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3514 self.treeviewEpisodes.set_rules_hint( True)
3515 self.treeviewEpisodes.set_model( self.model)
3516 self.treeviewEpisodes.columns_autosize()
3517 self.calculate_total_size()
3519 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3520 # With get_bin_window, we get the window that contains the rows without
3521 # the header. The Y coordinate of this window will be the height of the
3522 # treeview header. This is the amount we have to subtract from the
3523 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3524 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3525 y -= x_bin
3526 y -= y_bin
3527 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3529 if not self.episode_list_can_tooltip:
3530 self.last_tooltip_episode = None
3531 return False
3533 if path is not None:
3534 model = treeview.get_model()
3535 iter = model.get_iter(path)
3536 index = model.get_value(iter, self.COLUMN_INDEX)
3537 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3538 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3539 self.last_tooltip_episode = None
3540 return False
3541 self.last_tooltip_episode = index
3543 if description is not None:
3544 tooltip.set_text(description)
3545 return True
3546 else:
3547 return False
3549 self.last_tooltip_episode = None
3550 return False
3552 def treeview_episodes_button_pressed(self, treeview, event):
3553 if event.button == 3:
3554 menu = gtk.Menu()
3556 if len(self.selection_buttons):
3557 for label in self.selection_buttons:
3558 item = gtk.MenuItem(label)
3559 item.connect('activate', self.custom_selection_button_clicked, label)
3560 menu.append(item)
3561 menu.append(gtk.SeparatorMenuItem())
3563 item = gtk.MenuItem(_('Select all'))
3564 item.connect('activate', self.on_btnCheckAll_clicked)
3565 menu.append(item)
3567 item = gtk.MenuItem(_('Select none'))
3568 item.connect('activate', self.on_btnCheckNone_clicked)
3569 menu.append(item)
3571 menu.show_all()
3572 # Disable tooltips while we are showing the menu, so
3573 # the tooltip will not appear over the menu
3574 self.episode_list_can_tooltip = False
3575 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3576 menu.popup(None, None, None, event.button, event.time)
3578 return True
3580 def episode_list_allow_tooltips(self):
3581 self.episode_list_can_tooltip = True
3583 def calculate_total_size( self):
3584 if self.size_attribute is not None:
3585 (total_size, count) = (0, 0)
3586 for episode in self.get_selected_episodes():
3587 try:
3588 total_size += int(getattr( episode, self.size_attribute))
3589 count += 1
3590 except:
3591 log( 'Cannot get size for %s', episode.title, sender = self)
3593 text = []
3594 if count == 0:
3595 text.append(_('Nothing selected'))
3596 elif count == 1:
3597 text.append(_('One episode selected'))
3598 else:
3599 text.append(_('%d episodes selected') % count)
3600 if total_size > 0:
3601 text.append(_('total size: %s') % gl.format_filesize(total_size))
3602 self.labelTotalSize.set_text(', '.join(text))
3603 self.btnOK.set_sensitive(count>0)
3604 self.btnRemoveAction.set_sensitive(count>0)
3605 if count > 0:
3606 self.btnCancel.set_label(gtk.STOCK_CANCEL)
3607 else:
3608 self.btnCancel.set_label(gtk.STOCK_CLOSE)
3609 else:
3610 self.btnOK.set_sensitive(False)
3611 self.btnRemoveAction.set_sensitive(False)
3612 for index, row in enumerate(self.model):
3613 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3614 self.btnOK.set_sensitive(True)
3615 self.btnRemoveAction.set_sensitive(True)
3616 break
3617 self.labelTotalSize.set_text('')
3619 def toggle_cell_handler( self, cell, path):
3620 model = self.treeviewEpisodes.get_model()
3621 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3623 self.calculate_total_size()
3625 def custom_selection_button_clicked(self, button, label):
3626 callback = self.selection_buttons[label]
3628 for index, row in enumerate( self.model):
3629 new_value = callback( self.episodes[index])
3630 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3632 self.calculate_total_size()
3634 def on_btnCheckAll_clicked( self, widget):
3635 for row in self.model:
3636 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3638 self.calculate_total_size()
3640 def on_btnCheckNone_clicked( self, widget):
3641 for row in self.model:
3642 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3644 self.calculate_total_size()
3646 def on_remove_action_activate(self, widget):
3647 episodes = self.get_selected_episodes(remove_episodes=True)
3649 for episode in episodes:
3650 self.remove_callback(episode)
3652 if self.remove_finished is not None:
3653 self.remove_finished()
3654 self.calculate_total_size()
3656 def get_selected_episodes( self, remove_episodes=False):
3657 selected_episodes = []
3659 for index, row in enumerate( self.model):
3660 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3661 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
3663 if remove_episodes:
3664 for episode in selected_episodes:
3665 index = self.episodes.index(episode)
3666 iter = self.model.get_iter_first()
3667 while iter is not None:
3668 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
3669 self.model.remove(iter)
3670 break
3671 iter = self.model.iter_next(iter)
3673 return selected_episodes
3675 def on_btnOK_clicked( self, widget):
3676 self.gPodderEpisodeSelector.destroy()
3677 if self.callback is not None:
3678 self.callback( self.get_selected_episodes())
3680 def on_btnCancel_clicked( self, widget):
3681 self.gPodderEpisodeSelector.destroy()
3682 if self.callback is not None:
3683 self.callback([])
3685 class gPodderConfigEditor(GladeWidget):
3686 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3688 def new(self):
3689 name_column = gtk.TreeViewColumn(_('Setting'))
3690 name_renderer = gtk.CellRendererText()
3691 name_column.pack_start(name_renderer)
3692 name_column.add_attribute(name_renderer, 'text', 0)
3693 name_column.add_attribute(name_renderer, 'style', 5)
3694 self.configeditor.append_column(name_column)
3696 value_column = gtk.TreeViewColumn(_('Set to'))
3697 value_check_renderer = gtk.CellRendererToggle()
3698 value_column.pack_start(value_check_renderer, expand=False)
3699 value_column.add_attribute(value_check_renderer, 'active', 7)
3700 value_column.add_attribute(value_check_renderer, 'visible', 6)
3701 value_column.add_attribute(value_check_renderer, 'activatable', 6)
3702 value_check_renderer.connect('toggled', self.value_toggled)
3704 value_renderer = gtk.CellRendererText()
3705 value_column.pack_start(value_renderer)
3706 value_column.add_attribute(value_renderer, 'text', 2)
3707 value_column.add_attribute(value_renderer, 'visible', 4)
3708 value_column.add_attribute(value_renderer, 'editable', 4)
3709 value_column.add_attribute(value_renderer, 'style', 5)
3710 value_renderer.connect('edited', self.value_edited)
3711 self.configeditor.append_column(value_column)
3713 self.model = gl.config.model()
3714 self.filter = self.model.filter_new()
3715 self.filter.set_visible_func(self.visible_func)
3717 self.configeditor.set_model(self.filter)
3718 self.configeditor.set_rules_hint(True)
3720 def visible_func(self, model, iter, user_data=None):
3721 text = self.entryFilter.get_text().lower()
3722 if text == '':
3723 return True
3724 else:
3725 # either the variable name or its value
3726 return (text in model.get_value(iter, 0).lower() or
3727 text in model.get_value(iter, 2).lower())
3729 def value_edited(self, renderer, path, new_text):
3730 model = self.configeditor.get_model()
3731 iter = model.get_iter(path)
3732 name = model.get_value(iter, 0)
3733 type_cute = model.get_value(iter, 1)
3735 if not gl.config.update_field(name, new_text):
3736 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))
3738 def value_toggled(self, renderer, path):
3739 model = self.configeditor.get_model()
3740 iter = model.get_iter(path)
3741 field_name = model.get_value(iter, 0)
3742 field_type = model.get_value(iter, 3)
3744 # Flip the boolean config flag
3745 if field_type == bool:
3746 gl.config.toggle_flag(field_name)
3748 def on_entryFilter_changed(self, widget):
3749 self.filter.refilter()
3751 def on_btnShowAll_clicked(self, widget):
3752 self.entryFilter.set_text('')
3753 self.entryFilter.grab_focus()
3755 def on_btnClose_clicked(self, widget):
3756 self.gPodderConfigEditor.destroy()
3758 class gPodderPlaylist(GladeWidget):
3759 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
3761 def new(self):
3762 self.m3u_header = '#EXTM3U\n'
3763 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
3764 if self.mountpoint == '/':
3765 self.mountpoint = gl.config.mp3_player_folder
3766 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
3767 self.playlist_file = os.path.join(self.mountpoint,
3768 gl.config.mp3_player_playlist_file)
3769 icon_theme = gtk.icon_theme_get_default()
3770 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
3772 # add column two
3773 check_cell = gtk.CellRendererToggle()
3774 check_cell.set_property('activatable', True)
3775 check_cell.connect('toggled', self.cell_toggled)
3776 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
3777 self.treeviewPlaylist.append_column(check_column)
3779 # add column three
3780 column = gtk.TreeViewColumn(_('Filename'))
3781 icon_cell = gtk.CellRendererPixbuf()
3782 column.pack_start(icon_cell, False)
3783 column.add_attribute(icon_cell, 'pixbuf', 0)
3784 filename_cell = gtk.CellRendererText()
3785 column.pack_start(filename_cell, True)
3786 column.add_attribute(filename_cell, 'text', 2)
3788 column.set_resizable(True)
3789 self.treeviewPlaylist.append_column(column)
3791 # Make treeview reorderable
3792 self.treeviewPlaylist.set_reorderable(True)
3794 # init liststore
3795 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
3796 self.treeviewPlaylist.set_model(self.playlist)
3798 # read device and playlist and fill the TreeView
3799 self.m3u = self.read_m3u()
3800 self.device = self.read_device()
3801 self.write2gui()
3803 def cell_toggled(self, cellrenderertoggle, path):
3804 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
3805 it = liststore.get_iter(path)
3806 liststore.set_value(it, 1, not liststore.get_value(it, 1))
3808 def on_btnCancelPlaylist_clicked(self, widget):
3809 self.gPodderPlaylist.destroy()
3811 def on_btnSavePlaylist_clicked(self, widget):
3812 self.write_m3u()
3813 self.gPodderPlaylist.destroy()
3815 def read_m3u(self):
3817 read all files from the existing playlist
3819 tracks = []
3820 if os.path.exists(self.playlist_file):
3821 for line in open(self.playlist_file, 'r'):
3822 if line != self.m3u_header:
3823 if line.startswith('#'):
3824 tracks.append([False, line[1:].strip()])
3825 else:
3826 tracks.append([True, line.strip()])
3827 return tracks
3829 def write_m3u(self):
3831 write the list into the playlist on the device
3833 playlist_folder = os.path.split(self.playlist_file)[0]
3834 if not util.make_directory(playlist_folder):
3835 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
3836 else:
3837 try:
3838 fp = open(self.playlist_file, 'w')
3839 fp.write(self.m3u_header)
3840 for icon, checked, filename in self.playlist:
3841 if not checked:
3842 fp.write('#')
3843 fp.write(filename)
3844 fp.write('\n')
3845 fp.close()
3846 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
3847 except IOError, ioe:
3848 self.show_message(str(ioe), _('Error writing playlist file'))
3850 def read_device(self):
3852 read all files from the device
3854 tracks = []
3855 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
3856 for file in files:
3857 filename = os.path.join(root, file)
3859 if filename == self.playlist_file:
3860 # We don't want to have our playlist file as
3861 # an entry in our file list, so skip it!
3862 break
3864 if not gl.config.mp3_player_playlist_absolute_path:
3865 filename = filename[len(self.mountpoint):]
3867 if gl.config.mp3_player_playlist_win_path:
3868 filename = filename.replace( '/', '\\')
3870 tracks.append(filename)
3871 return tracks
3873 def write2gui(self):
3874 # add the files from the device to the list only when
3875 # they are not yet in the playlist
3876 # mark this files as NEW
3877 for filename in self.device[:]:
3878 m3ulist = [file[1] for file in self.m3u]
3879 if filename not in m3ulist:
3880 self.playlist.append([self.icon_new, False, filename])
3882 # add the files from the playlist to the list only when
3883 # they are on the device
3884 for checked, filename in self.m3u[:]:
3885 if filename in self.device:
3886 self.playlist.append([None, checked, filename])
3888 class gPodderDependencyManager(GladeWidget):
3889 def new(self):
3890 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
3891 self.treeview_components.append_column(col_name)
3892 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
3893 self.treeview_components.append_column(col_installed)
3894 self.treeview_components.set_model(services.dependency_manager.get_model())
3895 self.btn_about.set_sensitive(False)
3897 def on_btn_about_clicked(self, widget):
3898 selection = self.treeview_components.get_selection()
3899 model, iter = selection.get_selected()
3900 if iter is not None:
3901 title = model.get_value(iter, 0)
3902 description = model.get_value(iter, 1)
3903 available = model.get_value(iter, 3)
3904 missing = model.get_value(iter, 4)
3906 if not available:
3907 description += '\n\n'+_('Missing components:')+'\n\n'+missing
3909 self.show_message(description, title)
3911 def on_btn_install_clicked(self, widget):
3912 # TODO: Implement package manager integration
3913 pass
3915 def on_treeview_components_cursor_changed(self, treeview):
3916 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
3917 # TODO: If installing is possible, enable btn_install
3919 def on_gPodderDependencyManager_response(self, dialog, response_id):
3920 self.gPodderDependencyManager.destroy()
3922 def main():
3923 gobject.threads_init()
3924 gtk.window_set_default_icon_name( 'gpodder')
3926 gPodder().run()