Don't cancel feed update when a podcast fails (bug 521)
[gpodder.git] / src / gpodder / gui.py
blob3dc6c70f67da05c512fcc2b34d3ba7853071f769
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 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
33 import fnmatch
34 import tempfile
36 from xml.sax import saxutils
38 from threading import Event
39 from threading import Thread
40 from threading import Semaphore
41 from string import strip
43 import gpodder
45 try:
46 import dbus
47 import dbus.service
48 import dbus.mainloop
49 import dbus.glib
50 except ImportError:
51 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
52 class dbus:
53 class SessionBus:
54 def __init__(self, *args, **kwargs):
55 pass
56 class glib:
57 class DBusGMainLoop:
58 pass
59 class service:
60 @staticmethod
61 def method(interface):
62 return lambda x: x
63 class BusName:
64 def __init__(self, *args, **kwargs):
65 pass
66 class Object:
67 def __init__(self, *args, **kwargs):
68 pass
71 from gpodder import feedcore
72 from gpodder import util
73 from gpodder import opml
74 from gpodder import services
75 from gpodder import sync
76 from gpodder import download
77 from gpodder import my
78 from gpodder import widgets
79 from gpodder.liblogger import log
80 from gpodder import resolver
82 _ = gpodder.gettext
84 try:
85 from gpodder import trayicon
86 have_trayicon = True
87 except Exception, exc:
88 log('Warning: Could not import gpodder.trayicon.', traceback=True)
89 log('Warning: This probably means your PyGTK installation is too old!')
90 have_trayicon = False
92 from gpodder.model import PodcastChannel
94 from gpodder.gtkui.base import GtkBuilderWidget
95 from gpodder.gtkui.model import PodcastListModel
96 from gpodder.gtkui.model import EpisodeListModel
97 from gpodder.gtkui.opml import OpmlListModel
99 from gpodder.libgpodder import db
100 from gpodder.libgpodder import gl
102 from libplayers import UserAppsReader
104 if gpodder.interface == gpodder.GUI:
105 WEB_BROWSER_ICON = 'web-browser'
106 elif gpodder.interface == gpodder.MAEMO:
107 import hildon
108 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
110 app_authors = [
111 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
113 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
114 'Alain Tauch', 'Alex Ghitza', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
115 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
116 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier', 'Casey Watson',
117 'Carlos Moffat', 'Chris Arnold', 'Chris Moffitt', 'Clark Burbidge', 'Corey Goldberg', 'corq', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
118 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'Fabio Fiorentini', 'FFranci72', 'Florian Richter', 'Frank Harper',
119 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
120 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Iwan van der Kleijn', 'Jens Thiele',
121 'Jérôme Chabod', 'Jerry Moss',
122 'Jessica Henline', 'Jim Nygård', 'João Trindade', 'Joel Calado', 'John Ferguson',
123 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
124 'Jürgen Schinker', 'Justin Forest',
125 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marco Antonio Villegas Vega', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
126 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Morten Juhl-Johansen Zölde-Fejér', 'Mykola Nikishov', 'narf',
127 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
128 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
129 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
130 'Preben Randhol', 'Rafael Proença', 'R.Bell', 'red26wings', 'Richard Voigt',
131 'Robert Young', 'Roel Groeneveld', 'Romain Janvier',
132 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
133 'S. Rust',
134 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
135 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
136 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
137 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
139 'List may be incomplete - please contact me.'
142 class BuilderWidget(GtkBuilderWidget):
143 gpodder_main_window = None
144 finger_friendly_widgets = []
146 def __init__( self, **kwargs):
147 GtkBuilderWidget.__init__(self, gpodder.ui_folder, gpodder.textdomain, **kwargs)
149 # Set widgets to finger-friendly mode if on Maemo
150 for widget_name in self.finger_friendly_widgets:
151 if hasattr(self, widget_name):
152 self.set_finger_friendly(getattr(self, widget_name))
153 else:
154 log('Finger-friendly widget not found: %s', widget_name, sender=self)
156 if self.__class__.__name__ == 'gPodder':
157 BuilderWidget.gpodder_main_window = self.gPodder
158 else:
159 # If we have a child window, set it transient for our main window
160 self.main_window.set_transient_for(BuilderWidget.gpodder_main_window)
162 if gpodder.interface == gpodder.GUI:
163 if hasattr(self, 'center_on_widget'):
164 (x, y) = self.gpodder_main_window.get_position()
165 a = self.center_on_widget.allocation
166 (x, y) = (x + a.x, y + a.y)
167 (w, h) = (a.width, a.height)
168 (pw, ph) = self.main_window.get_size()
169 self.main_window.move(x + w/2 - pw/2, y + h/2 - ph/2)
170 else:
171 self.main_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
173 def notification(self, message, title=None):
174 util.idle_add(self.show_message, message, title)
176 def show_message( self, message, title = None):
177 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
178 if title is None:
179 title = 'gPodder'
180 self.tray_icon.send_notification(message, title)
181 return
183 if gpodder.interface == gpodder.GUI:
184 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
185 if title:
186 dlg.set_title(str(title))
187 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
188 else:
189 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
190 elif gpodder.interface == gpodder.MAEMO:
191 dlg = hildon.Note('information', (BuilderWidget.gpodder_main_window, message))
193 dlg.run()
194 dlg.destroy()
196 def set_finger_friendly(self, widget):
198 If we are on Maemo, we carry out the necessary
199 operations to turn a widget into a finger-friendly
200 one, depending on which type of widget it is (i.e.
201 buttons will have more padding, TreeViews a thick
202 scrollbar, etc..)
204 if widget is None:
205 return None
207 if gpodder.interface == gpodder.MAEMO:
208 if isinstance(widget, gtk.Misc):
209 widget.set_padding(0, 5)
210 elif isinstance(widget, gtk.Button):
211 for child in widget.get_children():
212 if isinstance(child, gtk.Alignment):
213 child.set_padding(5, 5, 5, 5)
214 else:
215 child.set_padding(5, 5)
216 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
217 parent = widget.get_parent()
218 if isinstance(parent, gtk.ScrolledWindow):
219 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
220 elif isinstance(widget, gtk.MenuItem):
221 for child in widget.get_children():
222 self.set_finger_friendly(child)
223 submenu = widget.get_submenu()
224 if submenu is not None:
225 for child in submenu.get_children():
226 self.set_finger_friendly(child)
227 elif isinstance(widget, gtk.Menu):
228 for child in widget.get_children():
229 self.set_finger_friendly(child)
230 else:
231 log('Cannot set widget finger-friendly: %s', widget, sender=self)
233 return widget
235 def show_confirmation( self, message, title = None):
236 if gpodder.interface == gpodder.GUI:
237 affirmative = gtk.RESPONSE_YES
238 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
239 if title:
240 dlg.set_title(str(title))
241 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
242 else:
243 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
244 elif gpodder.interface == gpodder.MAEMO:
245 affirmative = gtk.RESPONSE_OK
246 dlg = hildon.Note('confirmation', (BuilderWidget.gpodder_main_window, message))
248 response = dlg.run()
249 dlg.destroy()
251 return response == affirmative
253 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
254 """ An authentication dialog based on
255 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
257 dialog = gtk.MessageDialog(
258 BuilderWidget.gpodder_main_window,
259 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
260 gtk.MESSAGE_QUESTION,
261 gtk.BUTTONS_OK_CANCEL )
263 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
265 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
266 dialog.set_title(_('Authentication required'))
267 dialog.format_secondary_markup(message)
268 dialog.set_default_response(gtk.RESPONSE_OK)
270 if register_callback is not None:
271 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
273 username_entry = gtk.Entry()
274 password_entry = gtk.Entry()
276 username_entry.connect('activate', lambda w: password_entry.grab_focus())
277 password_entry.set_visibility(False)
278 password_entry.set_activates_default(True)
280 if username is not None:
281 username_entry.set_text(username)
282 if password is not None:
283 password_entry.set_text(password)
285 table = gtk.Table(2, 2)
286 table.set_row_spacings(6)
287 table.set_col_spacings(6)
289 username_label = gtk.Label()
290 username_label.set_markup('<b>' + username_prompt + ':</b>')
291 username_label.set_alignment(0.0, 0.5)
292 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
293 table.attach(username_entry, 1, 2, 0, 1)
295 password_label = gtk.Label()
296 password_label.set_markup('<b>' + _('Password') + ':</b>')
297 password_label.set_alignment(0.0, 0.5)
298 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
299 table.attach(password_entry, 1, 2, 1, 2)
301 dialog.vbox.pack_end(table, True, True, 0)
302 dialog.show_all()
303 response = dialog.run()
305 while response == gtk.RESPONSE_HELP:
306 register_callback()
307 response = dialog.run()
309 password_entry.set_visibility(True)
310 dialog.destroy()
312 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
314 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
315 if dst_filename is None:
316 dst_filename = src_filename
318 if dst_directory is None:
319 dst_directory = os.path.expanduser( '~')
321 ( base, extension ) = os.path.splitext( src_filename)
323 if not dst_filename.endswith( extension):
324 dst_filename += extension
326 if gpodder.interface == gpodder.GUI:
327 dlg = gtk.FileChooserDialog(title=title, parent=BuilderWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
328 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
329 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
330 elif gpodder.interface == gpodder.MAEMO:
331 dlg = hildon.FileChooserDialog(BuilderWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
333 dlg.set_do_overwrite_confirmation( True)
334 dlg.set_current_name( os.path.basename( dst_filename))
335 dlg.set_current_folder( dst_directory)
337 result = False
338 folder = dst_directory
339 if dlg.run() == gtk.RESPONSE_OK:
340 result = True
341 dst_filename = dlg.get_filename()
342 folder = dlg.get_current_folder()
343 if not dst_filename.endswith( extension):
344 dst_filename += extension
346 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
348 try:
349 shutil.copyfile( src_filename, dst_filename)
350 except:
351 log( 'Error copying file.', sender = self, traceback = True)
353 dlg.destroy()
354 return (result, folder)
357 class gPodder(BuilderWidget, dbus.service.Object):
358 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'btnCleanUpDownloads']
359 ENTER_URL_TEXT = _('Enter podcast URL...')
360 APPMENU_ACTIONS = ('itemUpdate', 'itemDownloadAllNew', 'itemPreferences')
361 TREEVIEW_WIDGETS = ('treeAvailable', 'treeChannels', 'treeDownloads')
363 def __init__(self, bus_name):
364 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
365 BuilderWidget.__init__(self)
367 def new(self):
368 if gpodder.interface == gpodder.MAEMO:
369 # Maemo-specific changes to the UI
370 gpodder.icon_file = gpodder.icon_file.replace('.svg', '.png')
372 self.app = hildon.Program()
373 gtk.set_application_name('gPodder')
374 self.window = hildon.Window()
375 self.window.connect('delete-event', self.on_gPodder_delete_event)
376 self.window.connect('window-state-event', self.window_state_event)
378 self.itemUpdateChannel.set_visible(True)
380 # Remove old toolbar from its parent widget
381 self.toolbar.get_parent().remove(self.toolbar)
383 toolbar = gtk.Toolbar()
384 toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
386 self.btnUpdateFeeds.get_parent().remove(self.btnUpdateFeeds)
388 self.btnUpdateFeeds = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update all'))
389 self.btnUpdateFeeds.set_is_important(True)
390 self.btnUpdateFeeds.connect('clicked', self.on_itemUpdate_activate)
391 toolbar.insert(self.btnUpdateFeeds, -1)
392 self.btnUpdateFeeds.show_all()
394 self.btnUpdateSelectedFeed = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update selected'))
395 self.btnUpdateSelectedFeed.set_is_important(True)
396 self.btnUpdateSelectedFeed.connect('clicked', self.on_itemUpdateChannel_activate)
397 toolbar.insert(self.btnUpdateSelectedFeed, -1)
398 self.btnUpdateSelectedFeed.show_all()
400 self.toolFeedUpdateProgress = gtk.ToolItem()
401 self.pbFeedUpdate.reparent(self.toolFeedUpdateProgress)
402 self.toolFeedUpdateProgress.set_expand(True)
403 toolbar.insert(self.toolFeedUpdateProgress, -1)
404 self.toolFeedUpdateProgress.hide()
406 self.btnCancelFeedUpdate = gtk.ToolButton(gtk.STOCK_CLOSE)
407 self.btnCancelFeedUpdate.connect('clicked', self.on_btnCancelFeedUpdate_clicked)
408 toolbar.insert(self.btnCancelFeedUpdate, -1)
409 self.btnCancelFeedUpdate.hide()
411 self.toolbarSpacer = gtk.SeparatorToolItem()
412 self.toolbarSpacer.set_draw(False)
413 self.toolbarSpacer.set_expand(True)
414 toolbar.insert(self.toolbarSpacer, -1)
415 self.toolbarSpacer.show()
417 self.wNotebook.set_show_tabs(False)
418 self.tool_downloads = gtk.ToggleToolButton(gtk.STOCK_GO_DOWN)
419 self.tool_downloads.connect('toggled', self.on_tool_downloads_toggled)
420 self.tool_downloads.set_label(_('Downloads'))
421 self.tool_downloads.set_is_important(True)
422 toolbar.insert(self.tool_downloads, -1)
423 self.tool_downloads.show_all()
425 self.toolPreferences = gtk.ToolButton(gtk.STOCK_PREFERENCES)
426 self.toolPreferences.connect('clicked', self.on_itemPreferences_activate)
427 toolbar.insert(self.toolPreferences, -1)
428 self.toolPreferences.show()
430 self.toolQuit = gtk.ToolButton(gtk.STOCK_QUIT)
431 self.toolQuit.connect('clicked', self.on_gPodder_delete_event)
432 toolbar.insert(self.toolQuit, -1)
433 self.toolQuit.show()
435 # Add and replace toolbar with our new one
436 toolbar.show()
437 self.window.add_toolbar(toolbar)
438 self.toolbar = toolbar
440 self.app.add_window(self.window)
441 self.vMain.reparent(self.window)
442 self.gPodder = self.window
444 # Reparent the main menu
445 menu = gtk.Menu()
446 for child in self.mainMenu.get_children():
447 child.get_parent().remove(child)
448 menu.append(self.set_finger_friendly(child))
449 menu.append(self.set_finger_friendly(self.itemQuit.create_menu_item()))
451 if hasattr(hildon, 'AppMenu'):
452 # Maemo 5 - use the new AppMenu with Buttons
453 self.appmenu = hildon.AppMenu()
454 for action_name in self.APPMENU_ACTIONS:
455 action = getattr(self, action_name)
456 b = gtk.Button('')
457 action.connect_proxy(b)
458 self.appmenu.append(b)
459 b = gtk.Button(_('Classic menu'))
460 b.connect('clicked', lambda b: menu.popup(None, None, None, 1, 0))
461 self.appmenu.append(b)
462 self.window.set_app_menu(self.appmenu)
463 else:
464 # Maemo 4 - just "reparent" the menu to the hildon window
465 self.window.set_menu(menu)
467 self.mainMenu.destroy()
468 self.window.show()
470 # do some widget hiding
471 self.itemTransferSelected.set_visible(False)
472 self.item_email_subscriptions.set_visible(False)
473 self.menuView.set_visible(False)
475 # get screen real estate
476 self.hboxContainer.set_border_width(0)
478 # Offer importing of videocenter podcasts
479 if os.path.exists(os.path.expanduser('~/videocenter')):
480 self.item_upgrade_from_videocenter.set_visible(True)
482 self.gPodder.connect('key-press-event', self.on_key_press)
483 self.bluetooth_available = util.bluetooth_available()
485 if gpodder.win32:
486 # FIXME: Implement e-mail sending of list in win32
487 self.item_email_subscriptions.set_sensitive(False)
489 if gl.config.show_url_entry_in_podcast_list:
490 self.hboxAddChannel.show()
492 if not gpodder.interface == gpodder.MAEMO and not gl.config.show_toolbar:
493 self.toolbar.hide()
495 gl.config.add_observer(self.on_config_changed)
496 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
497 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
498 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
499 self.entry_add_channel_unfocus(self.entryAddChannel, None)
501 self.uar = None
502 self.tray_icon = None
503 self.gpodder_episode_window = None
505 self.download_status_manager = services.DownloadStatusManager()
506 self.download_queue_manager = download.DownloadQueueManager(self.download_status_manager, gl.config)
508 self.fullscreen = False
509 self.minimized = False
510 self.gPodder.connect('window-state-event', self.window_state_event)
512 self.show_hide_tray_icon()
514 self.itemShowToolbar.set_active(gl.config.show_toolbar)
515 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
517 gl.config.connect_gtk_window(self.gPodder, 'main_window')
518 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
520 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
521 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
522 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
523 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
525 # Then the amount of maximum downloads changes, notify the queue manager
526 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
527 self.spinMaxDownloads.connect('value-changed', changed_cb)
529 self.default_title = None
530 if gpodder.__version__.rfind('git') != -1:
531 self.set_title('gPodder %s' % gpodder.__version__)
532 else:
533 title = self.gPodder.get_title()
534 if title is not None:
535 self.set_title(title)
536 else:
537 self.set_title(_('gPodder'))
539 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
541 # Set up podcast channel tree view widget
542 self.treeChannels.set_enable_search(True)
543 self.treeChannels.set_search_column(PodcastListModel.C_TITLE)
544 self.treeChannels.set_headers_visible(False)
546 iconcolumn = gtk.TreeViewColumn('')
547 iconcell = gtk.CellRendererPixbuf()
548 iconcolumn.pack_start(iconcell, False)
549 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
550 self.treeChannels.append_column(iconcolumn)
552 namecolumn = gtk.TreeViewColumn('')
553 namecell = gtk.CellRendererText()
554 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
555 namecolumn.pack_start(namecell, True)
556 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
558 iconcell = gtk.CellRendererPixbuf()
559 iconcell.set_property('xalign', 1.0)
560 namecolumn.pack_start(iconcell, False)
561 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
562 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
563 self.treeChannels.append_column(namecolumn)
565 # Generate list models for podcasts and their episodes
566 self.podcast_list_model = PodcastListModel(gl.config.podcast_list_icon_size)
567 self.treeChannels.set_model(self.podcast_list_model)
569 self.episode_list_model = EpisodeListModel()
570 self.treeAvailable.set_model(self.episode_list_model)
572 # enable alternating colors hint
573 self.treeAvailable.set_rules_hint( True)
574 self.treeChannels.set_rules_hint( True)
576 # connect to tooltip signals
577 try:
578 self.treeChannels.set_property('has-tooltip', True)
579 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
580 self.treeAvailable.set_property('has-tooltip', True)
581 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
582 except:
583 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
584 self.last_tooltip_channel = None
585 self.last_tooltip_episode = None
586 self.podcast_list_can_tooltip = True
587 self.episode_list_can_tooltip = True
589 self.currently_updating = False
591 # Add our context menu to treeAvailable
592 if gpodder.interface == gpodder.MAEMO:
593 self.treeview_available_buttonpress = (0, 0)
594 self.treeAvailable.connect('button-press-event', self.treeview_button_savepos)
595 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
597 self.treeview_channels_buttonpress = (0, 0)
598 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
599 self.treeChannels.connect('button-release-event', self.treeview_channels_button_released)
600 else:
601 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
602 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
604 self.treeDownloads.connect('button-press-event', self.treeview_downloads_button_pressed)
606 iconcell = gtk.CellRendererPixbuf()
607 if gpodder.interface == gpodder.MAEMO:
608 iconcell.set_fixed_size(-1, 52)
609 status_column_label = ''
610 else:
611 status_column_label = _('Status')
612 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
614 namecell = gtk.CellRendererText()
615 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
616 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
617 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
618 namecolumn.set_resizable(True)
619 namecolumn.set_expand(True)
621 sizecell = gtk.CellRendererText()
622 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
624 releasecell = gtk.CellRendererText()
625 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
627 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
628 itemcolumn.set_reorderable(gpodder.interface != gpodder.MAEMO)
629 self.treeAvailable.append_column(itemcolumn)
631 if gpodder.interface == gpodder.MAEMO:
632 # Due to screen space contraints, we
633 # hide these columns here by default
634 self.column_size = sizecolumn
635 self.column_released = releasecolumn
636 self.column_released.set_visible(False)
637 self.column_size.set_visible(False)
639 # enable search in treeavailable
640 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
642 # on Maemo 5, we need to set hildon-ui-mode of TreeView widgets to 1
643 if gpodder.interface == gpodder.MAEMO:
644 HUIM = 'hildon-ui-mode'
645 if HUIM in [p.name for p in gobject.list_properties(gtk.TreeView)]:
646 for treeview_name in self.TREEVIEW_WIDGETS:
647 treeview = getattr(self, treeview_name)
648 treeview.set_property(HUIM, 1)
650 # enable multiple selection support
651 if gpodder.interface == gpodder.MAEMO:
652 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_SINGLE)
653 else:
654 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
655 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
657 if hasattr(self.treeDownloads, 'set_rubber_banding'):
658 # Available in PyGTK 2.10 and above
659 self.treeDownloads.set_rubber_banding(True)
661 # columns and renderers for "download progress" tab
662 DownloadStatusManager = services.DownloadStatusManager
664 # First column: [ICON] Episodename
665 column = gtk.TreeViewColumn(_('Episode'))
667 cell = gtk.CellRendererPixbuf()
668 if gpodder.interface == gpodder.MAEMO:
669 cell.set_property('stock-size', gtk.ICON_SIZE_DIALOG)
670 else:
671 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
672 column.pack_start(cell, expand=False)
673 column.add_attribute(cell, 'stock-id', \
674 DownloadStatusManager.C_ICON_NAME)
676 cell = gtk.CellRendererText()
677 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
678 column.pack_start(cell, expand=True)
679 column.add_attribute(cell, 'text', DownloadStatusManager.C_NAME)
681 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
682 column.set_resizable(True)
683 column.set_expand(True)
684 self.treeDownloads.append_column(column)
686 # Second column: Progress
687 column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
688 value=DownloadStatusManager.C_PROGRESS, \
689 text=DownloadStatusManager.C_PROGRESS_TEXT)
690 self.treeDownloads.append_column(column)
692 # Third column: Size
693 if gpodder.interface != gpodder.MAEMO:
694 column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
695 text=DownloadStatusManager.C_SIZE_TEXT)
696 self.treeDownloads.append_column(column)
698 # Fourth column: Speed
699 column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
700 text=DownloadStatusManager.C_SPEED_TEXT)
701 self.treeDownloads.append_column(column)
703 # Fifth column: Status
704 column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
705 text=DownloadStatusManager.C_STATUS_TEXT)
706 self.treeDownloads.append_column(column)
708 # After we've set up most of the window, show it :)
709 if not gpodder.interface == gpodder.MAEMO:
710 self.gPodder.show()
712 if gl.config.start_iconified:
713 self.iconify_main_window()
714 if self.tray_icon and gl.config.minimize_to_tray:
715 self.tray_icon.set_visible(False)
717 services.cover_downloader.register('cover-available', self.cover_download_finished)
718 services.cover_downloader.register('cover-removed', self.cover_file_removed)
720 self.treeDownloads.set_model(self.download_status_manager.get_tree_model())
721 self.download_tasks_seen = set()
722 self.download_list_update_enabled = False
723 self.last_download_count = 0
725 #Add Drag and Drop Support
726 flags = gtk.DEST_DEFAULT_ALL
727 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
728 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
729 self.treeChannels.drag_dest_set( flags, targets, actions)
730 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
732 # Subscribed channels
733 self.active_channel = None
734 self.channels = PodcastChannel.load_from_db(db, gl.config.download_dir)
735 self.channel_list_changed = True
736 self.update_podcasts_tab()
738 # load list of user applications for audio playback
739 self.user_apps_reader = UserAppsReader(['audio', 'video'])
740 Thread(target=self.read_apps).start()
742 # Set the "Device" menu item for the first time
743 self.update_item_device()
745 # Last folder used for saving episodes
746 self.folder_for_saving_episodes = None
748 # Now, update the feed cache, when everything's in place
749 self.btnUpdateFeeds.show()
750 self.updating_feed_cache = False
751 self.feed_cache_update_cancelled = False
752 self.update_feed_cache(force_update=gl.config.update_on_startup)
754 # Look for partial file downloads
755 partial_files = glob.glob(os.path.join(gl.config.download_dir, '*', '*.partial'))
757 # Message area
758 self.message_area = None
760 resumable_episodes = []
761 if len(partial_files) > 0:
762 for f in partial_files:
763 correct_name = f[:-len('.partial')] # strip ".partial"
764 log('Searching episode for file: %s', correct_name, sender=self)
765 found_episode = False
766 for c in self.channels:
767 for e in c.get_all_episodes():
768 if e.local_filename(create=False, check_only=True) == correct_name:
769 log('Found episode: %s', e.title, sender=self)
770 resumable_episodes.append(e)
771 found_episode = True
772 if found_episode:
773 break
774 if found_episode:
775 break
776 if not found_episode:
777 log('Partial file without episode: %s', f, sender=self)
778 util.delete_file(f)
780 if len(resumable_episodes):
781 self.download_episode_list_paused(resumable_episodes)
782 self.message_area = widgets.SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
783 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
784 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
785 self.message_area.show_all()
786 self.wNotebook.set_current_page(1)
788 gl.clean_up_downloads(delete_partial=False)
789 else:
790 gl.clean_up_downloads(delete_partial=True)
792 # Start the auto-update procedure
793 self.auto_update_procedure(first_run=True)
795 # Delete old episodes if the user wishes to
796 if gl.config.auto_remove_old_episodes:
797 old_episodes = self.get_old_episodes()
798 if len(old_episodes) > 0:
799 self.delete_episode_list(old_episodes, confirm=False)
800 self.updateComboBox()
802 # First-time users should be asked if they want to see the OPML
803 if len(self.channels) == 0:
804 util.idle_add(self.on_itemUpdate_activate)
806 def enable_download_list_update(self):
807 if not self.download_list_update_enabled:
808 gobject.timeout_add(1500, self.update_downloads_list)
809 self.download_list_update_enabled = True
811 def on_btnCleanUpDownloads_clicked(self, button):
812 model = self.treeDownloads.get_model()
814 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
815 changed_episode_urls = []
816 for row_reference, task in all_tasks:
817 if task.status in (task.DONE, task.CANCELLED, task.FAILED):
818 model.remove(model.get_iter(row_reference.get_path()))
819 try:
820 # We don't "see" this task anymore - remove it;
821 # this is needed, so update_episode_list_icons()
822 # below gets the correct list of "seen" tasks
823 self.download_tasks_seen.remove(task)
824 except KeyError, key_error:
825 log('Cannot remove task from "seen" list: %s', task, sender=self)
826 changed_episode_urls.append(task.url)
827 # Tell the task that it has been removed (so it can clean up)
828 task.removed_from_list()
830 # Tell the podcasts tab to update icons for our removed podcasts
831 self.update_episode_list_icons(changed_episode_urls)
833 # Update the tab title and downloads list
834 self.update_downloads_list()
836 def on_tool_downloads_toggled(self, toolbutton):
837 if toolbutton.get_active():
838 self.wNotebook.set_current_page(1)
839 else:
840 self.wNotebook.set_current_page(0)
842 def update_downloads_list(self):
843 try:
844 model = self.treeDownloads.get_model()
846 downloading, failed, finished, queued, others = 0, 0, 0, 0, 0
847 total_speed, total_size, done_size = 0, 0, 0
849 # Keep a list of all download tasks that we've seen
850 download_tasks_seen = set()
852 # Remember the progress and speed for the episode that
853 # has been opened in the episode shownotes dialog (if any)
854 if self.gpodder_episode_window is not None:
855 episode_window_episode = self.gpodder_episode_window.episode
856 episode_window_progress = 0.0
857 episode_window_speed = 0.0
858 else:
859 episode_window_episode = None
861 # Do not go through the list of the model is not (yet) available
862 if model is None:
863 model = ()
865 for row in model:
866 self.download_status_manager.request_update(row.iter)
868 task = row[self.download_status_manager.C_TASK]
869 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
871 total_size += size
872 done_size += size*progress
874 if episode_window_episode is not None and \
875 episode_window_episode.url == task.url:
876 episode_window_progress = progress
877 episode_window_speed = speed
879 download_tasks_seen.add(task)
881 if status == download.DownloadTask.DOWNLOADING:
882 downloading += 1
883 total_speed += speed
884 elif status == download.DownloadTask.FAILED:
885 failed += 1
886 elif status == download.DownloadTask.DONE:
887 finished += 1
888 elif status == download.DownloadTask.QUEUED:
889 queued += 1
890 else:
891 others += 1
893 # Remember which tasks we have seen after this run
894 self.download_tasks_seen = download_tasks_seen
896 text = [_('Downloads')]
897 if downloading + failed + finished + queued > 0:
898 s = []
899 if downloading > 0:
900 s.append(_('%d downloading') % downloading)
901 if failed > 0:
902 s.append(_('%d failed') % failed)
903 if finished > 0:
904 s.append(_('%d done') % finished)
905 if queued > 0:
906 s.append(_('%d queued') % queued)
907 text.append(' (' + ', '.join(s)+')')
908 self.labelDownloads.set_text(''.join(text))
910 if gpodder.interface == gpodder.MAEMO:
911 sum = downloading + failed + finished + queued + others
912 if sum:
913 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
914 else:
915 self.tool_downloads.set_label(_('Downloads'))
917 title = [self.default_title]
919 # We have to update all episodes/channels for which the status has
920 # changed. Accessing task.status_changed has the side effect of
921 # re-setting the changed flag, so we need to get the "changed" list
922 # of tuples first and split it into two lists afterwards
923 changed = [(task.url, task.podcast_url) for task in \
924 self.download_tasks_seen if task.status_changed]
925 episode_urls = [episode_url for episode_url, channel_url in changed]
926 channel_urls = [channel_url for episode_url, channel_url in changed]
928 count = downloading + queued
929 if count > 0:
930 if count == 1:
931 title.append( _('downloading one file'))
932 elif count > 1:
933 title.append( _('downloading %d files') % count)
935 if total_size > 0:
936 percentage = 100.0*done_size/total_size
937 else:
938 percentage = 0.0
939 total_speed = util.format_filesize(total_speed)
940 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
941 if self.tray_icon is not None:
942 # Update the tray icon status and progress bar
943 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
944 self.tray_icon.draw_progress_bar(percentage/100.)
945 elif self.last_download_count > 0:
946 if self.tray_icon is not None:
947 # Update the tray icon status
948 self.tray_icon.set_status()
949 self.tray_icon.downloads_finished(self.download_tasks_seen)
950 if gpodder.interface == gpodder.MAEMO:
951 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
952 log('All downloads have finished.', sender=self)
953 if gl.config.cmd_all_downloads_complete:
954 util.run_external_command(gl.config.cmd_all_downloads_complete)
955 self.last_download_count = count
957 self.gPodder.set_title(' - '.join(title))
959 self.update_episode_list_icons(episode_urls)
960 if self.gpodder_episode_window is not None and \
961 self.gpodder_episode_window.gPodderEpisode.get_property('visible'):
962 self.gpodder_episode_window.download_status_changed(episode_urls)
963 self.gpodder_episode_window.download_status_progress(episode_window_progress, episode_window_speed)
964 self.play_or_download()
965 if channel_urls:
966 self.updateComboBox(only_these_urls=channel_urls)
968 if not self.download_queue_manager.are_queued_or_active_tasks():
969 self.download_list_update_enabled = False
971 return self.download_list_update_enabled
972 except Exception, e:
973 log('Exception happened while updating download list.', sender=self, traceback=True)
974 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'))
975 # We return False here, so the update loop won't be called again,
976 # that's why we require the restart of gPodder in the message.
977 return False
979 def entry_add_channel_focus(self, widget, event):
980 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
981 if widget.get_text() == self.ENTER_URL_TEXT:
982 widget.set_text('')
984 def entry_add_channel_unfocus(self, widget, event):
985 if widget.get_text() == '':
986 widget.set_text(self.ENTER_URL_TEXT)
987 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
989 def on_config_changed(self, name, old_value, new_value):
990 if name == 'show_toolbar' and gpodder.interface != gpodder.MAEMO:
991 if new_value:
992 self.toolbar.show()
993 else:
994 self.toolbar.hide()
995 elif name == 'episode_list_descriptions' and gpodder.interface != gpodder.MAEMO:
996 self.updateTreeView()
997 elif name == 'show_url_entry_in_podcast_list':
998 if new_value:
999 self.hboxAddChannel.show()
1000 else:
1001 self.hboxAddChannel.hide()
1003 def read_apps(self):
1004 time.sleep(3) # give other parts of gpodder a chance to start up
1005 self.user_apps_reader.read()
1006 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
1007 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
1009 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1010 # With get_bin_window, we get the window that contains the rows without
1011 # the header. The Y coordinate of this window will be the height of the
1012 # treeview header. This is the amount we have to subtract from the
1013 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1014 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1015 y -= x_bin
1016 y -= y_bin
1017 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1019 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
1020 self.last_tooltip_episode = None
1021 return False
1023 if path is not None:
1024 model = treeview.get_model()
1025 iter = model.get_iter(path)
1026 url = model.get_value(iter, EpisodeListModel.C_URL)
1027 description = model.get_value(iter, EpisodeListModel.C_DESCRIPTION_STRIPPED)
1028 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
1029 self.last_tooltip_episode = None
1030 return False
1031 self.last_tooltip_episode = url
1033 if len(description) > 400:
1034 description = description[:398]+'[...]'
1036 tooltip.set_text(description)
1037 return True
1039 self.last_tooltip_episode = None
1040 return False
1042 def podcast_list_allow_tooltips(self):
1043 self.podcast_list_can_tooltip = True
1045 def episode_list_allow_tooltips(self):
1046 self.episode_list_can_tooltip = True
1048 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1049 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1051 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
1052 self.last_tooltip_channel = None
1053 return False
1055 if path is not None:
1056 model = treeview.get_model()
1057 iter = model.get_iter(path)
1058 url = model.get_value(iter, PodcastListModel.C_URL)
1059 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1061 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
1062 self.last_tooltip_channel = None
1063 return False
1064 self.last_tooltip_channel = channel
1065 channel.request_save_dir_size()
1066 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1067 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1068 if error_str:
1069 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1070 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1071 table = gtk.Table(rows=3, columns=3)
1072 table.set_row_spacings(5)
1073 table.set_col_spacings(5)
1074 table.set_border_width(5)
1076 heading = gtk.Label()
1077 heading.set_alignment(0, 1)
1078 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1079 table.attach(heading, 0, 1, 0, 1)
1080 size_info = gtk.Label()
1081 size_info.set_alignment(1, 1)
1082 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1083 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1084 table.attach(size_info, 2, 3, 0, 1)
1086 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1088 if len(channel.description) < 500:
1089 description = channel.description
1090 else:
1091 pos = channel.description.find('\n\n')
1092 if pos == -1 or pos > 500:
1093 description = channel.description[:498]+'[...]'
1094 else:
1095 description = channel.description[:pos]
1097 description = gtk.Label(description)
1098 if error_str:
1099 description.set_markup(error_str)
1100 description.set_alignment(0, 0)
1101 description.set_line_wrap(True)
1102 table.attach(description, 0, 3, 2, 3)
1104 table.show_all()
1105 tooltip.set_custom(table)
1107 return True
1109 self.last_tooltip_channel = None
1110 return False
1112 def update_m3u_playlist_clicked(self, widget):
1113 self.active_channel.update_m3u_playlist()
1114 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
1116 def treeview_downloads_button_pressed(self, treeview, event):
1117 if event.button == 1:
1118 # Catch left mouse button presses, and if we there is no
1119 # path at the given position, deselect all items
1120 (x, y) = (int(event.x), int(event.y))
1121 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1122 if path is None:
1123 treeview.get_selection().unselect_all()
1125 # Use right-click for the Desktop version and left-click for Maemo
1126 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1127 (event.button == 3 and gpodder.interface == gpodder.GUI):
1128 (x, y) = (int(event.x), int(event.y))
1129 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1131 paths = []
1132 # Did the user right-click into a selection?
1133 selection = treeview.get_selection()
1134 if selection.count_selected_rows() and path:
1135 (model, paths) = selection.get_selected_rows()
1136 if path not in paths:
1137 # We have right-clicked, but not into the
1138 # selection, assume we don't want to operate
1139 # on the selection
1140 paths = []
1142 # No selection or right click not in selection:
1143 # Select the single item where we clicked
1144 if not paths and path:
1145 treeview.grab_focus()
1146 treeview.set_cursor( path, column, 0)
1147 (model, paths) = (treeview.get_model(), [path])
1149 # We did not find a selection, and the user didn't
1150 # click on an item to select -- don't show the menu
1151 if not paths:
1152 return True
1154 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
1156 def make_menu_item(label, stock_id, tasks, status):
1157 # This creates a menu item for selection-wide actions
1158 def for_each_task_set_status(tasks, status):
1159 changed_episode_urls = []
1160 for row_reference, task in tasks:
1161 if status is not None:
1162 if status == download.DownloadTask.QUEUED:
1163 # Only queue task when its paused/failed/cancelled
1164 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1165 self.download_queue_manager.add_task(task)
1166 self.enable_download_list_update()
1167 elif status == download.DownloadTask.CANCELLED:
1168 # Cancelling a download only allows when paused/downloading/queued
1169 if task.status in (task.QUEUED, task.DOWNLOADING, task.PAUSED):
1170 task.status = status
1171 elif status == download.DownloadTask.PAUSED:
1172 # Pausing a download only when queued/downloading
1173 if task.status in (task.DOWNLOADING, task.QUEUED):
1174 task.status = status
1175 else:
1176 # We (hopefully) can simply set the task status here
1177 task.status = status
1178 else:
1179 # Remove the selected task - cancel downloading/queued tasks
1180 if task.status in (task.QUEUED, task.DOWNLOADING):
1181 task.status = task.CANCELLED
1182 model.remove(model.get_iter(row_reference.get_path()))
1183 # Remember the URL, so we can tell the UI to update
1184 try:
1185 # We don't "see" this task anymore - remove it;
1186 # this is needed, so update_episode_list_icons()
1187 # below gets the correct list of "seen" tasks
1188 self.download_tasks_seen.remove(task)
1189 except KeyError, key_error:
1190 log('Cannot remove task from "seen" list: %s', task, sender=self)
1191 changed_episode_urls.append(task.url)
1192 # Tell the task that it has been removed (so it can clean up)
1193 task.removed_from_list()
1194 # Tell the podcasts tab to update icons for our removed podcasts
1195 self.update_episode_list_icons(changed_episode_urls)
1196 # Update the tab title and downloads list
1197 self.update_downloads_list()
1198 return True
1199 item = gtk.ImageMenuItem(label)
1200 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1201 item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
1203 # Determine if we should disable this menu item
1204 for row_reference, task in tasks:
1205 if status == download.DownloadTask.QUEUED:
1206 if task.status not in (download.DownloadTask.PAUSED, \
1207 download.DownloadTask.FAILED, \
1208 download.DownloadTask.CANCELLED):
1209 item.set_sensitive(False)
1210 break
1211 elif status == download.DownloadTask.CANCELLED:
1212 if task.status not in (download.DownloadTask.PAUSED, \
1213 download.DownloadTask.QUEUED, \
1214 download.DownloadTask.DOWNLOADING):
1215 item.set_sensitive(False)
1216 break
1217 elif status == download.DownloadTask.PAUSED:
1218 if task.status not in (download.DownloadTask.QUEUED, \
1219 download.DownloadTask.DOWNLOADING):
1220 item.set_sensitive(False)
1221 break
1222 elif status is None:
1223 if task.status not in (download.DownloadTask.CANCELLED, \
1224 download.DownloadTask.FAILED, \
1225 download.DownloadTask.DONE):
1226 item.set_sensitive(False)
1227 break
1229 return self.set_finger_friendly(item)
1231 menu = gtk.Menu()
1233 item = gtk.ImageMenuItem(_('Episode details'))
1234 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1235 if len(selected_tasks) == 1:
1236 row_reference, task = selected_tasks[0]
1237 episode = task.episode
1238 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1239 else:
1240 item.set_sensitive(False)
1241 menu.append(item)
1242 menu.append(gtk.SeparatorMenuItem())
1243 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
1244 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
1245 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
1246 menu.append(gtk.SeparatorMenuItem())
1247 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
1249 if gpodder.interface == gpodder.MAEMO:
1250 # Because we open the popup on left-click for Maemo,
1251 # we also include a non-action to close the menu
1252 menu.append(gtk.SeparatorMenuItem())
1253 item = gtk.ImageMenuItem(_('Close this menu'))
1254 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1255 menu.append(self.set_finger_friendly(item))
1257 menu.show_all()
1258 menu.popup(None, None, None, event.button, event.time)
1259 return True
1261 def treeview_channels_button_pressed( self, treeview, event):
1262 global WEB_BROWSER_ICON
1264 if gpodder.interface == gpodder.MAEMO:
1265 self.treeview_channels_buttonpress = (event.x, event.y)
1266 return True
1268 if event.button == 3:
1269 ( x, y ) = ( int(event.x), int(event.y) )
1270 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1272 paths = []
1274 # Did the user right-click into a selection?
1275 selection = treeview.get_selection()
1276 if selection.count_selected_rows() and path:
1277 ( model, paths ) = selection.get_selected_rows()
1278 if path not in paths:
1279 # We have right-clicked, but not into the
1280 # selection, assume we don't want to operate
1281 # on the selection
1282 paths = []
1284 # No selection or right click not in selection:
1285 # Select the single item where we clicked
1286 if not len( paths) and path:
1287 treeview.grab_focus()
1288 treeview.set_cursor( path, column, 0)
1290 ( model, paths ) = ( treeview.get_model(), [ path ] )
1292 # We did not find a selection, and the user didn't
1293 # click on an item to select -- don't show the menu
1294 if not len( paths):
1295 return True
1297 menu = gtk.Menu()
1299 item = gtk.ImageMenuItem( _('Open download folder'))
1300 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
1301 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1302 menu.append( item)
1304 item = gtk.ImageMenuItem( _('Update Feed'))
1305 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
1306 item.connect('activate', self.on_itemUpdateChannel_activate )
1307 item.set_sensitive( not self.updating_feed_cache )
1308 menu.append( item)
1310 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1311 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1312 item.connect('activate', self.update_m3u_playlist_clicked)
1313 menu.append(item)
1315 if self.active_channel.link:
1316 item = gtk.ImageMenuItem(_('Visit website'))
1317 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1318 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1319 menu.append(item)
1321 if self.active_channel.channel_is_locked:
1322 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1323 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1324 item.connect('activate', self.on_channel_toggle_lock_activate)
1325 menu.append(self.set_finger_friendly(item))
1326 else:
1327 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1328 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1329 item.connect('activate', self.on_channel_toggle_lock_activate)
1330 menu.append(self.set_finger_friendly(item))
1333 menu.append( gtk.SeparatorMenuItem())
1335 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1336 item.connect( 'activate', self.on_itemEditChannel_activate)
1337 menu.append( item)
1339 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1340 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1341 menu.append( item)
1343 menu.show_all()
1344 # Disable tooltips while we are showing the menu, so
1345 # the tooltip will not appear over the menu
1346 self.podcast_list_can_tooltip = False
1347 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
1348 menu.popup( None, None, None, event.button, event.time)
1350 return True
1352 def on_itemClose_activate(self, widget):
1353 if self.tray_icon is not None:
1354 if gpodder.interface == gpodder.MAEMO:
1355 self.gPodder.set_property('visible', False)
1356 else:
1357 self.iconify_main_window()
1358 else:
1359 self.on_gPodder_delete_event(widget)
1361 def cover_file_removed(self, channel_url):
1363 The Cover Downloader calls this when a previously-
1364 available cover has been removed from the disk. We
1365 have to update our model to reflect this change.
1367 self.podcast_list_model.delete_cover_by_url(channel_url)
1369 def cover_download_finished(self, channel_url, pixbuf):
1371 The Cover Downloader calls this when it has finished
1372 downloading (or registering, if already downloaded)
1373 a new channel cover, which is ready for displaying.
1375 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1377 def save_episode_as_file(self, episode):
1378 if episode.was_downloaded(and_exists=True):
1379 folder = self.folder_for_saving_episodes
1380 copy_from = episode.local_filename(create=False)
1381 assert copy_from is not None
1382 copy_to = episode.sync_filename(gl.config.custom_sync_name_enabled, gl.config.custom_sync_name)
1383 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1384 self.folder_for_saving_episodes = folder
1386 def copy_episodes_bluetooth(self, episodes):
1387 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1389 def convert_and_send_thread(episodes, notify):
1390 for episode in episodes:
1391 filename = episode.local_filename(create=False)
1392 assert filename is not None
1393 destfile = os.path.join(tempfile.gettempdir(), \
1394 util.sanitize_filename(episode.sync_filename(gl.config.custom_sync_name_enabled, gl.config.custom_sync_name)))
1395 (base, ext) = os.path.splitext(filename)
1396 if not destfile.endswith(ext):
1397 destfile += ext
1399 try:
1400 shutil.copyfile(filename, destfile)
1401 util.bluetooth_send_file(destfile)
1402 except:
1403 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1404 notify(_('Error converting file.'), _('Bluetooth file transfer'))
1406 util.delete_file(destfile)
1408 Thread(target=convert_and_send_thread, args=[episodes_to_copy, self.notification]).start()
1410 def treeview_button_savepos(self, treeview, event):
1411 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1412 self.treeview_available_buttonpress = (event.x, event.y)
1413 return True
1415 def treeview_channels_button_released(self, treeview, event):
1416 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1417 selection = self.treeChannels.get_selection()
1418 pathatpos = self.treeChannels.get_path_at_pos(int(event.x), int(event.y))
1419 if self.currently_updating:
1420 log('do not handle press while updating', sender=self)
1421 return True
1422 if pathatpos is None:
1423 return False
1424 else:
1425 ydistance = int(abs(event.y-self.treeview_channels_buttonpress[1]))
1426 xdistance = int(event.x-self.treeview_channels_buttonpress[0])
1427 if ydistance < 30:
1428 (path, column, x, y) = pathatpos
1429 selection.select_path(path)
1430 self.treeChannels.set_cursor(path)
1431 self.treeChannels.grab_focus()
1432 # Emulate the cursor changed signal to force an update
1433 self.on_treeChannels_cursor_changed(self.treeChannels)
1434 return True
1436 def get_device_name(self):
1437 if gl.config.device_type == 'ipod':
1438 return _('iPod')
1439 elif gl.config.device_type in ('filesystem', 'mtp'):
1440 return _('MP3 player')
1441 else:
1442 return '(unknown device)'
1444 def treeview_button_pressed( self, treeview, event):
1445 global WEB_BROWSER_ICON
1447 if gpodder.interface == gpodder.MAEMO:
1448 ydistance = int(abs(event.y-self.treeview_available_buttonpress[1]))
1449 xdistance = int(event.x-self.treeview_available_buttonpress[0])
1451 selection = self.treeAvailable.get_selection()
1452 pathatpos = self.treeAvailable.get_path_at_pos(int(event.x), int(event.y))
1453 if pathatpos is None:
1454 # No item at the current cursor position
1455 return False
1456 elif ydistance < 30:
1457 # Item under the cursor, and no scrolling done
1458 (path, column, x, y) = pathatpos
1459 selection.select_path(path)
1460 self.treeAvailable.set_cursor(path)
1461 self.treeAvailable.grab_focus()
1462 if gl.config.maemo_enable_gestures and xdistance > 70:
1463 self.on_playback_selected_episodes(None)
1464 return True
1465 elif gl.config.maemo_enable_gestures and xdistance < -70:
1466 self.on_shownotes_selected_episodes(None)
1467 return True
1468 else:
1469 # Scrolling has been done
1470 return True
1472 # Use right-click for the Desktop version and left-click for Maemo
1473 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1474 (event.button == 3 and gpodder.interface == gpodder.GUI):
1475 ( x, y ) = ( int(event.x), int(event.y) )
1476 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1478 paths = []
1480 # Did the user right-click into a selection?
1481 selection = self.treeAvailable.get_selection()
1482 if selection.count_selected_rows() and path:
1483 ( model, paths ) = selection.get_selected_rows()
1484 if path not in paths:
1485 # We have right-clicked, but not into the
1486 # selection, assume we don't want to operate
1487 # on the selection
1488 paths = []
1490 # No selection or right click not in selection:
1491 # Select the single item where we clicked
1492 if not len( paths) and path:
1493 treeview.grab_focus()
1494 treeview.set_cursor( path, column, 0)
1496 ( model, paths ) = ( treeview.get_model(), [ path ] )
1498 # We did not find a selection, and the user didn't
1499 # click on an item to select -- don't show the menu
1500 if not len( paths):
1501 return True
1503 episodes = self.get_selected_episodes()
1504 any_locked = any(e.is_locked for e in episodes)
1505 any_played = any(e.is_played for e in episodes)
1506 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1508 menu = gtk.Menu()
1510 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1512 if open_instead_of_play:
1513 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1514 else:
1515 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1517 item.set_sensitive(can_play)
1518 item.connect('activate', self.on_playback_selected_episodes)
1519 menu.append(self.set_finger_friendly(item))
1521 if not can_cancel:
1522 item = gtk.ImageMenuItem(_('Download'))
1523 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1524 item.set_sensitive(can_download)
1525 item.connect('activate', self.on_download_selected_episodes)
1526 menu.append(self.set_finger_friendly(item))
1527 else:
1528 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1529 item.connect('activate', lambda w: self.on_treeDownloads_row_activated(self.toolCancel))
1530 menu.append(self.set_finger_friendly(item))
1532 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1533 item.set_sensitive(can_delete)
1534 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1535 menu.append(self.set_finger_friendly(item))
1537 if one_is_new:
1538 item = gtk.ImageMenuItem(_('Do not download'))
1539 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1540 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1541 menu.append(self.set_finger_friendly(item))
1542 elif can_download:
1543 item = gtk.ImageMenuItem(_('Mark as new'))
1544 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1545 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1546 menu.append(self.set_finger_friendly(item))
1548 # Ok, this probably makes sense to only display for downloaded files
1549 if can_play and not can_download:
1550 menu.append( gtk.SeparatorMenuItem())
1551 item = gtk.ImageMenuItem(_('Save to disk'))
1552 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1553 item.connect('activate', lambda w: [self.save_episode_as_file(e) for e in episodes])
1554 menu.append(self.set_finger_friendly(item))
1555 if self.bluetooth_available:
1556 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1557 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1558 item.connect('activate', lambda w: self.copy_episodes_bluetooth(episodes))
1559 menu.append(self.set_finger_friendly(item))
1560 if can_transfer:
1561 item = gtk.ImageMenuItem(_('Transfer to %s') % self.get_device_name())
1562 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1563 item.connect('activate', lambda w: self.on_sync_to_ipod_activate(w, episodes))
1564 menu.append(self.set_finger_friendly(item))
1566 if can_play:
1567 menu.append( gtk.SeparatorMenuItem())
1568 if any_played:
1569 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1570 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1571 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1572 menu.append(self.set_finger_friendly(item))
1573 else:
1574 item = gtk.ImageMenuItem(_('Mark as played'))
1575 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1576 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1577 menu.append(self.set_finger_friendly(item))
1579 if any_locked:
1580 item = gtk.ImageMenuItem(_('Allow deletion'))
1581 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1582 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, False))
1583 menu.append(self.set_finger_friendly(item))
1584 else:
1585 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1586 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1587 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, True))
1588 menu.append(self.set_finger_friendly(item))
1590 menu.append(gtk.SeparatorMenuItem())
1591 # Single item, add episode information menu item
1592 item = gtk.ImageMenuItem(_('Episode details'))
1593 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1594 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1595 menu.append(self.set_finger_friendly(item))
1597 # If we have it, also add episode website link
1598 if episodes[0].link and episodes[0].link != episodes[0].url:
1599 item = gtk.ImageMenuItem(_('Visit website'))
1600 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1601 item.connect('activate', lambda w: util.open_website(episodes[0].link))
1602 menu.append(self.set_finger_friendly(item))
1604 if gpodder.interface == gpodder.MAEMO:
1605 # Because we open the popup on left-click for Maemo,
1606 # we also include a non-action to close the menu
1607 menu.append(gtk.SeparatorMenuItem())
1608 item = gtk.ImageMenuItem(_('Close this menu'))
1609 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1610 menu.append(self.set_finger_friendly(item))
1612 menu.show_all()
1613 # Disable tooltips while we are showing the menu, so
1614 # the tooltip will not appear over the menu
1615 self.episode_list_can_tooltip = False
1616 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1617 menu.popup( None, None, None, event.button, event.time)
1619 return True
1621 def set_title(self, new_title):
1622 self.default_title = new_title
1623 self.gPodder.set_title(new_title)
1625 def update_selected_episode_list_icons(self):
1627 Updates the status icons in the episode list
1629 selection = self.treeAvailable.get_selection()
1630 (model, paths) = selection.get_selected_rows()
1631 for path in paths:
1632 iter = model.get_iter(path)
1633 self.episode_list_model.update_by_iter(iter, \
1634 self.episode_is_downloading, \
1635 gl.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO)
1637 def update_episode_list_icons(self, urls):
1639 Updates the status icons in the episode list
1640 Only update the episodes that have an URL in
1641 the "urls" iterable object (e.g. a list of URLs)
1643 if self.active_channel is None or not urls:
1644 return
1646 self.episode_list_model.update_by_urls(urls, \
1647 self.episode_is_downloading, \
1648 gl.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO)
1650 def playback_episodes(self, episodes):
1651 if gpodder.interface == gpodder.MAEMO:
1652 if len(episodes) == 1:
1653 text = _('Opening %s') % saxutils.escape(episodes[0].title)
1654 else:
1655 text = _('Opening %d episodes') % len(episodes)
1656 banner = hildon.hildon_banner_show_animation(self.gPodder, None, text)
1657 def destroy_banner_later(banner):
1658 banner.destroy()
1659 return False
1660 gobject.timeout_add(5000, destroy_banner_later, banner)
1662 episodes = [e for e in episodes if \
1663 e.was_downloaded(and_exists=True) or gl.streaming_possible()]
1665 try:
1666 gl.playback_episodes(episodes)
1667 except Exception, e:
1668 log('Error in playback!', sender=self, traceback=True)
1669 self.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'))
1671 self.update_selected_episode_list_icons()
1672 self.updateComboBox(only_selected_channel=True)
1674 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1675 if model is None:
1676 return True
1678 key = key.lower()
1680 for column in (EpisodeListModel.C_TITLE, EpisodeListModel.C_DESCRIPTION_STRIPPED):
1681 value = model.get_value( iter, column).lower()
1682 if value.find( key) != -1:
1683 return False
1685 return True
1687 def change_menu_item(self, menuitem, icon=None, label=None):
1688 if icon is not None:
1689 menuitem.set_property('stock-id', icon)
1690 if label is not None:
1691 menuitem.label = label
1693 def play_or_download(self):
1694 if self.wNotebook.get_current_page() > 0:
1695 return
1697 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1698 ( is_played, is_locked ) = (False,)*2
1700 open_instead_of_play = False
1702 selection = self.treeAvailable.get_selection()
1703 if selection.count_selected_rows() > 0:
1704 (model, paths) = selection.get_selected_rows()
1706 for path in paths:
1707 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
1709 if episode.file_type() not in ('audio', 'video'):
1710 open_instead_of_play = True
1712 if episode.was_downloaded():
1713 can_play = episode.was_downloaded(and_exists=True)
1714 can_delete = True
1715 is_played = episode.is_played
1716 is_locked = episode.is_locked
1717 if not can_play:
1718 can_download = True
1719 else:
1720 if self.episode_is_downloading(episode):
1721 can_cancel = True
1722 else:
1723 can_download = True
1725 can_download = can_download and not can_cancel
1726 can_play = gl.streaming_possible() or (can_play and not can_cancel and not can_download)
1727 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1729 if open_instead_of_play:
1730 if gpodder.interface != gpodder.MAEMO:
1731 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1732 can_transfer = False
1733 else:
1734 if gpodder.interface != gpodder.MAEMO:
1735 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1737 self.toolPlay.set_sensitive( can_play)
1738 self.toolDownload.set_sensitive( can_download)
1739 self.toolTransfer.set_sensitive( can_transfer)
1740 self.toolCancel.set_sensitive( can_cancel)
1742 self.item_cancel_download.set_sensitive(can_cancel)
1743 self.itemDownloadSelected.set_sensitive(can_download)
1744 self.itemOpenSelected.set_sensitive(can_play)
1745 self.itemPlaySelected.set_sensitive(can_play)
1746 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1747 self.item_toggle_played.set_sensitive(can_play)
1748 self.item_toggle_lock.set_sensitive(can_play)
1750 self.itemOpenSelected.set_visible(open_instead_of_play)
1751 self.itemPlaySelected.set_visible(not open_instead_of_play)
1753 if can_play:
1754 if is_played:
1755 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1756 else:
1757 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1758 if is_locked:
1759 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1760 else:
1761 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1763 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1765 def on_cbMaxDownloads_toggled(self, widget, *args):
1766 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1768 def on_cbLimitDownloads_toggled(self, widget, *args):
1769 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1771 def episode_new_status_changed(self, urls):
1772 self.updateComboBox()
1773 self.update_episode_list_icons(urls)
1775 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1776 selection = self.treeChannels.get_selection()
1777 (model, iter) = selection.get_selected()
1779 if only_selected_channel:
1780 # very cheap! only update selected channel
1781 if iter and self.active_channel is not None:
1782 model.update_by_iter(iter)
1783 elif not self.channel_list_changed:
1784 # we can keep the model, but have to update some
1785 if only_these_urls is None:
1786 # still cheaper than reloading the whole list
1787 iter = model.get_iter_first()
1788 while iter is not None:
1789 model.update_by_iter(iter)
1790 iter = model.iter_next(iter)
1791 else:
1792 # ok, we got a bunch of urls to update
1793 model.update_by_urls(only_these_urls)
1794 else:
1795 if model and iter and selected_url is None:
1796 # Get the URL of the currently-selected podcast
1797 selected_url = model.get_value(iter, 0)
1799 # Update the podcast list model with new channels
1800 self.podcast_list_model.set_channels(self.channels)
1802 try:
1803 selected_path = (0,)
1804 # Find the previously-selected URL in the new
1805 # model if we have an URL (else select first)
1806 if selected_url is not None:
1807 pos = model.get_iter_first()
1808 while pos is not None:
1809 url = model.get_value(pos, 0)
1810 if url == selected_url:
1811 selected_path = model.get_path(pos)
1812 break
1813 pos = model.iter_next(pos)
1815 self.treeChannels.get_selection().select_path(selected_path)
1816 except:
1817 log( 'Cannot set selection on treeChannels', sender = self)
1818 self.on_treeChannels_cursor_changed( self.treeChannels)
1819 self.channel_list_changed = False
1821 def episode_is_downloading(self, episode):
1822 """Returns True if the given episode is being downloaded at the moment"""
1823 if episode is None:
1824 return False
1826 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1828 def on_episode_list_model_updated(self, banner=None):
1829 if banner is not None:
1830 banner.destroy()
1831 self.treeAvailable.columns_autosize()
1832 self.play_or_download()
1833 self.currently_updating = False
1835 def updateTreeView(self):
1836 if self.channels and self.active_channel is not None:
1837 if gpodder.interface == gpodder.MAEMO:
1838 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes for %s') % saxutils.escape(self.active_channel.title))
1839 else:
1840 banner = None
1842 self.currently_updating = True
1843 self.episode_list_model.update_from_channel(self.active_channel, \
1844 self.episode_is_downloading, \
1845 gl.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO, \
1846 lambda: self.on_episode_list_model_updated(banner))
1847 else:
1848 self.episode_list_model.clear()
1850 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1851 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1853 dnd_channel = None
1854 if path is not None:
1855 model = self.treeChannels.get_model()
1856 iter = model.get_iter(path)
1857 url = model.get_value(iter, 0)
1858 for channel in self.channels:
1859 if channel.url == url:
1860 dnd_channel = channel
1861 break
1863 result = sel.data
1864 rl = result.strip().lower()
1865 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1866 services.cover_downloader.replace_cover(dnd_channel, result)
1867 else:
1868 self.add_new_channel(result)
1870 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1871 result = util.normalize_feed_url(result)
1872 (scheme, rest) = result.split('://', 1)
1874 if not result:
1875 cute_scheme = saxutils.escape(scheme)+'://'
1876 title = _('%s URLs are not supported') % cute_scheme
1877 message = _('gPodder does not understand the URL you supplied.')
1878 self.show_message( message, title)
1879 return
1881 for old_channel in self.channels:
1882 if old_channel.url == result:
1883 log( 'Channel already exists: %s', result)
1884 # Select the existing channel in combo box
1885 for i in range( len( self.channels)):
1886 if self.channels[i] == old_channel:
1887 self.treeChannels.get_selection().select_path( (i,))
1888 self.on_treeChannels_cursor_changed(self.treeChannels)
1889 break
1890 self.show_message( _('You have already subscribed to this podcast: %s') % (
1891 saxutils.escape( old_channel.title), ), _('Already added'))
1892 return
1894 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1895 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1896 waitdlg.set_title(_('Downloading episode list'))
1897 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1898 waitdlg.format_secondary_text(_('Downloading episode information for %s') % result)
1899 waitpb = gtk.ProgressBar()
1900 if block:
1901 waitdlg.vbox.add(waitpb)
1902 waitdlg.show_all()
1903 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1905 self.entryAddChannel.set_text(_('Downloading feed...'))
1906 self.entryAddChannel.set_sensitive(False)
1907 self.btnAddChannel.set_sensitive(False)
1908 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1909 thread = Thread( target=self.add_new_channel_proc, args=args )
1910 thread.start()
1912 while block and thread.isAlive():
1913 while gtk.events_pending():
1914 gtk.main_iteration( False)
1915 waitpb.pulse()
1916 time.sleep(0.1)
1919 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1920 log( 'Adding new channel: %s', url)
1921 channel = error = None
1922 try:
1923 channel = PodcastChannel.load(db, url=url, create=True,
1924 authentication_tokens=authentication_tokens,
1925 max_episodes=gl.config.max_episodes_per_feed,
1926 download_dir=gl.config.download_dir)
1927 except feedcore.AuthenticationRequired, e:
1928 error = e
1929 except feedcore.WifiLogin, e:
1930 error = e
1931 except Exception, e:
1932 log('Error adding channel: %s', e, traceback=True, sender=self)
1934 util.idle_add( callback, channel, url, error, *callback_args )
1936 def save_channels_opml(self):
1937 exporter = opml.Exporter(gpodder.subscription_file)
1938 return exporter.write(self.channels)
1940 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1941 if channel is not None:
1942 self.channels.append( channel)
1943 self.channel_list_changed = True
1944 self.save_channels_opml()
1945 if not quiet:
1946 # download changed channels and select the new episode in the UI afterwards
1947 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1949 try:
1950 (username, password) = util.username_password_from_url(url)
1951 except ValueError, ve:
1952 self.show_message(_('The following error occured while trying to get authentication data from the URL:') + '\n\n' + ve.message, _('Error getting authentication data'))
1953 (username, password) = (None, None)
1954 log('Error getting authentication data from URL: %s', url, traceback=True)
1956 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')):
1957 channel.username = username
1958 channel.password = password
1959 log('Saving authentication data for episode downloads..', sender = self)
1960 channel.save()
1961 # We need to update the channel list otherwise the authentication
1962 # data won't show up in the channel editor.
1963 # TODO: Only updated the newly added feed to save some cpu cycles
1964 self.channels = PodcastChannel.load_from_db(db, gl.config.download_dir)
1965 self.channel_list_changed = True
1967 if ask_download_new:
1968 new_episodes = channel.get_new_episodes(downloading=self.episode_is_downloading)
1969 if len(new_episodes):
1970 self.new_episodes_show(new_episodes)
1972 elif isinstance(error, feedcore.AuthenticationRequired):
1973 response, auth_tokens = self.UsernamePasswordDialog(
1974 _('Feed requires authentication'), _('Please enter your username and password.'))
1976 if response:
1977 self.add_new_channel( url, authentication_tokens=auth_tokens )
1979 elif isinstance(error, feedcore.WifiLogin):
1980 if self.show_confirmation(_('The URL you are trying to add redirects to the website %s. Do you want to visit the website to login now?') % saxutils.escape(error.data), _('Website redirection detected')):
1981 util.open_website(error.data)
1982 if self.show_confirmation(_('Please login to the website now. Should I retry subscribing to the podcast at %s?') % saxutils.escape(url), _('Retry adding channel')):
1983 self.add_new_channel(url)
1985 else:
1986 # Ok, the URL is not a channel, or there is some other
1987 # error - let's see if it's a web page or OPML file...
1988 handled = False
1989 try:
1990 data = urllib2.urlopen(url).read().lower()
1991 if '</opml>' in data:
1992 # This looks like an OPML feed
1993 self.on_item_import_from_file_activate(None, url)
1994 handled = True
1996 elif '</html>' in data:
1997 # This looks like a web page
1998 title = _('The URL is a website')
1999 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.)')
2000 if self.show_confirmation(message, title):
2001 util.open_website(url)
2002 handled = True
2004 except Exception, e:
2005 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
2007 if not handled:
2008 title = _('Error adding podcast')
2009 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
2010 self.show_message( message, title)
2012 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
2013 self.entryAddChannel.set_sensitive(True)
2014 self.btnAddChannel.set_sensitive(True)
2015 self.update_podcasts_tab()
2016 waitdlg.destroy()
2019 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2020 db.commit()
2021 self.updating_feed_cache = False
2023 self.channels = PodcastChannel.load_from_db(db, gl.config.download_dir)
2024 self.channel_list_changed = True
2025 self.updateComboBox(selected_url=select_url_afterwards)
2027 # Only search for new episodes in podcasts that have been
2028 # updated, not in other podcasts (for single-feed updates)
2029 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2031 if self.tray_icon:
2032 self.tray_icon.set_status()
2034 if self.feed_cache_update_cancelled:
2035 # The user decided to abort the feed update
2036 self.show_update_feeds_buttons()
2037 elif not episodes:
2038 # Nothing new here - but inform the user
2039 self.pbFeedUpdate.set_fraction(1.0)
2040 self.pbFeedUpdate.set_text(_('No new episodes'))
2041 self.feed_cache_update_cancelled = True
2042 self.btnCancelFeedUpdate.show()
2043 self.btnCancelFeedUpdate.set_sensitive(True)
2044 if gpodder.interface == gpodder.MAEMO:
2045 # btnCancelFeedUpdate is a ToolButton on Maemo
2046 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2047 else:
2048 # btnCancelFeedUpdate is a normal gtk.Button
2049 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2050 else:
2051 # New episodes are available
2052 self.pbFeedUpdate.set_fraction(1.0)
2053 # Are we minimized and should we auto download?
2054 if (self.minimized and (gl.config.auto_download == 'minimized')) or (gl.config.auto_download == 'always'):
2055 self.download_episode_list(episodes)
2056 if len(episodes) == 1:
2057 title = _('Downloading one new episode')
2058 else:
2059 title = _('Downloading %d new episodes') % len(episodes)
2061 if self.tray_icon:
2062 message = self.tray_icon.format_episode_list([e.title for e in episodes])
2063 self.tray_icon.send_notification(message, title)
2064 self.show_update_feeds_buttons()
2065 else:
2066 self.show_update_feeds_buttons()
2067 # New episodes are available and we are not minimized
2068 if not gl.config.do_not_show_new_episodes_dialog:
2069 self.new_episodes_show(episodes)
2070 else:
2071 if len(episodes) == 1:
2072 message = _('One new episode is available for download')
2073 else:
2074 message = _('%i new episodes are available for download' % len(episodes))
2076 self.pbFeedUpdate.set_text(message)
2078 def update_feed_cache_proc(self, channels, select_url_afterwards):
2079 total = len(channels)
2081 for updated, channel in enumerate(channels):
2082 if not self.feed_cache_update_cancelled:
2083 try:
2084 channel.update(max_episodes=gl.config.max_episodes_per_feed)
2085 # except feedcore.Offline:
2086 # self.feed_cache_update_cancelled = True
2087 # if not self.minimized:
2088 # util.idle_add(self.show_message, _('The feed update has been cancelled because you appear to be offline.'), _('Cannot connect to server'))
2089 # break
2090 except Exception, e:
2091 util.idle_add(self.show_message, _('There has been an error updating %s: %s') % (saxutils.escape(channel.url), saxutils.escape(str(e))), _('Error while updating feed'))
2092 log('Error: %s', str(e), sender=self, traceback=True)
2094 # By the time we get here the update may have already been cancelled
2095 if not self.feed_cache_update_cancelled:
2096 def update_progress():
2097 progression = _('Updated %s (%d/%d)') % (channel.title, updated, total)
2098 self.pbFeedUpdate.set_text(progression)
2099 if self.tray_icon:
2100 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2101 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
2102 util.idle_add(update_progress)
2104 if self.feed_cache_update_cancelled:
2105 break
2107 updated_urls = [c.url for c in channels]
2108 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2110 def show_update_feeds_buttons(self):
2111 # Make sure that the buttons for updating feeds
2112 # appear - this should happen after a feed update
2113 if gpodder.interface == gpodder.MAEMO:
2114 self.btnUpdateSelectedFeed.show()
2115 self.toolFeedUpdateProgress.hide()
2116 self.btnCancelFeedUpdate.hide()
2117 self.btnCancelFeedUpdate.set_is_important(False)
2118 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2119 self.toolbarSpacer.set_expand(True)
2120 self.toolbarSpacer.set_draw(False)
2121 else:
2122 self.hboxUpdateFeeds.hide()
2123 self.btnUpdateFeeds.show()
2124 self.itemUpdate.set_sensitive(True)
2125 self.itemUpdateChannel.set_sensitive(True)
2127 def on_btnCancelFeedUpdate_clicked(self, widget):
2128 if not self.feed_cache_update_cancelled:
2129 self.pbFeedUpdate.set_text(_('Cancelling...'))
2130 self.feed_cache_update_cancelled = True
2131 self.btnCancelFeedUpdate.set_sensitive(False)
2132 else:
2133 self.show_update_feeds_buttons()
2135 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2136 if self.updating_feed_cache:
2137 return
2139 if not force_update:
2140 self.channels = PodcastChannel.load_from_db(db, gl.config.download_dir)
2141 self.channel_list_changed = True
2142 self.updateComboBox(selected_url=select_url_afterwards)
2143 return
2145 self.updating_feed_cache = True
2146 self.itemUpdate.set_sensitive(False)
2147 self.itemUpdateChannel.set_sensitive(False)
2149 if self.tray_icon:
2150 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2152 if channels is None:
2153 channels = self.channels
2155 if len(channels) == 1:
2156 text = _('Updating "%s"...') % channels[0].title
2157 else:
2158 text = _('Updating %d feeds...') % len(channels)
2159 self.pbFeedUpdate.set_text(text)
2160 self.pbFeedUpdate.set_fraction(0)
2162 self.feed_cache_update_cancelled = False
2163 self.btnCancelFeedUpdate.show()
2164 self.btnCancelFeedUpdate.set_sensitive(True)
2165 if gpodder.interface == gpodder.MAEMO:
2166 self.toolbarSpacer.set_expand(False)
2167 self.toolbarSpacer.set_draw(True)
2168 self.btnUpdateSelectedFeed.hide()
2169 self.toolFeedUpdateProgress.show_all()
2170 else:
2171 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2172 self.hboxUpdateFeeds.show_all()
2173 self.btnUpdateFeeds.hide()
2175 args = (channels, select_url_afterwards)
2176 Thread(target=self.update_feed_cache_proc, args=args).start()
2178 def on_gPodder_delete_event(self, widget, *args):
2179 """Called when the GUI wants to close the window
2180 Displays a confirmation dialog (and closes/hides gPodder)
2183 downloading = self.download_status_manager.are_downloads_in_progress()
2185 # Only iconify if we are using the window's "X" button,
2186 # but not when we are using "Quit" in the menu or toolbar
2187 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2188 self.iconify_main_window()
2189 elif gl.config.on_quit_ask or downloading:
2190 if gpodder.interface == gpodder.MAEMO:
2191 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2192 if result:
2193 self.close_gpodder()
2194 else:
2195 return True
2196 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2197 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2198 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2200 title = _('Quit gPodder')
2201 if downloading:
2202 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2203 else:
2204 message = _('Do you really want to quit gPodder now?')
2206 dialog.set_title(title)
2207 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2208 if not downloading:
2209 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2210 dialog.vbox.pack_start(cb_ask)
2211 cb_ask.show_all()
2213 result = dialog.run()
2214 dialog.destroy()
2216 if result == gtk.RESPONSE_CLOSE:
2217 if not downloading and cb_ask.get_active() == True:
2218 gl.config.on_quit_ask = False
2219 self.close_gpodder()
2220 else:
2221 self.close_gpodder()
2223 return True
2225 def close_gpodder(self):
2226 """ clean everything and exit properly
2228 if self.channels:
2229 if self.save_channels_opml():
2230 if gl.config.my_gpodder_autoupload:
2231 log('Uploading to my.gpodder.org on close', sender=self)
2232 util.idle_add(self.on_upload_to_mygpo, None)
2233 else:
2234 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
2236 self.gPodder.hide()
2238 if self.tray_icon is not None:
2239 self.tray_icon.set_visible(False)
2241 # Notify all tasks to to carry out any clean-up actions
2242 self.download_status_manager.tell_all_tasks_to_quit()
2244 while gtk.events_pending():
2245 gtk.main_iteration(False)
2247 db.close()
2249 self.quit()
2250 sys.exit(0)
2252 def get_old_episodes(self):
2253 episodes = []
2254 for channel in self.channels:
2255 for episode in channel.get_downloaded_episodes():
2256 if episode.age_in_days() > gl.config.episode_old_age and \
2257 not episode.is_locked and episode.is_played:
2258 episodes.append(episode)
2259 return episodes
2261 def delete_episode_list( self, episodes, confirm = True):
2262 if len(episodes) == 0:
2263 return
2265 if len(episodes) == 1:
2266 message = _('Do you really want to delete this episode?')
2267 else:
2268 message = _('Do you really want to delete %d episodes?') % len(episodes)
2270 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
2271 return
2273 episode_urls = set()
2274 channel_urls = set()
2275 for episode in episodes:
2276 log('Deleting episode: %s', episode.title, sender = self)
2277 episode.delete_from_disk()
2278 episode_urls.add(episode.url)
2279 channel_urls.add(episode.channel.url)
2281 # Episodes have been deleted - persist the database
2282 db.commit()
2284 self.update_episode_list_icons(episode_urls)
2285 self.updateComboBox(only_these_urls=channel_urls)
2287 def on_itemRemoveOldEpisodes_activate( self, widget):
2288 columns = (
2289 ('title_markup', None, None, _('Episode')),
2290 ('channel_prop', None, None, _('Podcast')),
2291 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2292 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2293 ('played_prop', None, None, _('Status')),
2294 ('age_prop', None, None, _('Downloaded')),
2297 selection_buttons = {
2298 _('Select played'): lambda episode: episode.is_played,
2299 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.age_in_days() > gl.config.episode_old_age,
2302 instructions = _('Select the episodes you want to delete from your hard disk.')
2304 episodes = []
2305 selected = []
2306 for channel in self.channels:
2307 for episode in channel.get_downloaded_episodes():
2308 if not episode.is_locked:
2309 episodes.append(episode)
2310 selected.append(episode.is_played)
2312 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
2313 episodes = episodes, selected = selected, columns = columns, \
2314 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2315 selection_buttons = selection_buttons)
2317 def on_selected_episodes_status_changed(self):
2318 self.update_selected_episode_list_icons()
2319 self.updateComboBox(only_selected_channel=True)
2320 db.commit()
2322 def mark_selected_episodes_new(self):
2323 for episode in self.get_selected_episodes():
2324 episode.mark_new()
2325 self.on_selected_episodes_status_changed()
2327 def mark_selected_episodes_old(self):
2328 for episode in self.get_selected_episodes():
2329 episode.mark_old()
2330 self.on_selected_episodes_status_changed()
2332 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2333 for episode in self.get_selected_episodes():
2334 if toggle:
2335 episode.mark(is_played=not episode.is_played)
2336 else:
2337 episode.mark(is_played=new_value)
2338 self.on_selected_episodes_status_changed()
2340 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2341 for episode in self.get_selected_episodes():
2342 if toggle:
2343 episode.mark(is_locked=not episode.is_locked)
2344 else:
2345 episode.mark(is_locked=new_value)
2346 self.on_selected_episodes_status_changed()
2348 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2349 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2350 self.active_channel.update_channel_lock()
2352 if self.active_channel.channel_is_locked:
2353 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2354 else:
2355 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2357 for episode in self.active_channel.get_all_episodes():
2358 episode.mark(is_locked=self.active_channel.channel_is_locked)
2360 self.updateComboBox(only_selected_channel=True)
2361 self.update_episode_list_icons([e.url for e in self.active_channel.get_all_episodes()])
2363 def send_subscriptions(self):
2364 try:
2365 subprocess.Popen(['xdg-email', '--subject', _('My podcast subscriptions'),
2366 '--attach', gpodder.subscription_file])
2367 except:
2368 return False
2370 return True
2372 def on_item_email_subscriptions_activate(self, widget):
2373 if not self.channels:
2374 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
2375 elif not self.send_subscriptions():
2376 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
2378 def on_itemUpdateChannel_activate(self, widget=None):
2379 self.update_feed_cache(channels=[self.active_channel,])
2381 def on_itemUpdate_activate(self, widget=None):
2382 if self.channels:
2383 self.update_feed_cache()
2384 else:
2385 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
2387 def download_episode_list_paused(self, episodes):
2388 self.download_episode_list(episodes, True)
2390 def download_episode_list(self, episodes, add_paused=False):
2391 for episode in episodes:
2392 log('Downloading episode: %s', episode.title, sender = self)
2393 if not episode.was_downloaded(and_exists=True):
2394 task_exists = False
2395 for task in self.download_tasks_seen:
2396 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2397 self.download_queue_manager.add_task(task)
2398 self.enable_download_list_update()
2399 task_exists = True
2400 continue
2402 if task_exists:
2403 continue
2405 try:
2406 task = download.DownloadTask(episode, gl.config)
2407 except Exception, e:
2408 self.show_message(_('Download error while downloading %s:\n\n%s') % (episode.title, str(e)), _('Download error'))
2409 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2410 continue
2412 if add_paused:
2413 task.status = task.PAUSED
2414 self.download_queue_manager.add_resumed_task(task)
2415 else:
2416 self.download_queue_manager.add_task(task)
2417 self.enable_download_list_update()
2419 def new_episodes_show(self, episodes):
2420 columns = (
2421 ('title_markup', None, None, _('Episode')),
2422 ('channel_prop', None, None, _('Podcast')),
2423 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2424 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2427 instructions = _('Select the episodes you want to download now.')
2429 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
2430 episodes=episodes, columns=columns, selected_default=True, \
2431 stock_ok_button = 'gpodder-download', \
2432 callback=self.download_episode_list, \
2433 remove_callback=lambda e: e.mark_old(), \
2434 remove_action=_('Never download'), \
2435 remove_finished=self.episode_new_status_changed)
2437 def on_itemDownloadAllNew_activate(self, widget, *args):
2438 new_episodes = self.get_new_episodes()
2439 if len(new_episodes):
2440 self.new_episodes_show(new_episodes)
2441 else:
2442 msg = _('No new episodes available for download')
2443 if self.tray_icon is not None and self.minimized:
2444 self.tray_icon.send_notification(msg)
2445 else:
2446 self.show_message(msg, _('No new episodes'))
2448 def get_new_episodes(self, channels=None):
2449 if channels is None:
2450 channels = self.channels
2451 episodes = []
2452 for channel in channels:
2453 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2454 episodes.append(episode)
2456 return episodes
2458 def get_all_episodes(self, exclude_nonsignificant=True ):
2459 """'exclude_nonsignificant' will exclude non-downloaded episodes
2460 and all episodes from channels that are set to skip when syncing"""
2461 episode_list = []
2462 for channel in self.channels:
2463 if not channel.sync_to_devices and exclude_nonsignificant:
2464 log('Skipping channel: %s', channel.title, sender=self)
2465 continue
2466 for episode in channel.get_all_episodes():
2467 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
2468 episode_list.append(episode)
2469 return episode_list
2471 def ipod_delete_played(self, device):
2472 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
2473 episodes_on_device = device.get_all_tracks()
2474 for local_episode in all_episodes:
2475 device_episode = device.episode_on_device(local_episode)
2476 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2477 or local_episode.state == gpodder.STATE_DELETED ):
2478 log("mp3_player_delete_played: removing %s" % device_episode.title)
2479 device.remove_track(device_episode)
2481 def on_sync_to_ipod_activate(self, widget, episodes=None):
2482 # make sure gpod is available before even trying to sync
2483 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2484 title = _('Cannot Sync To iPod')
2485 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2486 self.notification( message, title )
2487 return
2488 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2489 title = _('Cannot sync to MTP device')
2490 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2491 self.notification( message, title )
2492 return
2494 device = sync.open_device(gl.config)
2495 device.register( 'post-done', self.sync_to_ipod_completed )
2497 if device is None:
2498 title = _('No device configured')
2499 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2500 self.notification(message, title)
2501 return
2503 if not device.open():
2504 title = _('Cannot open device')
2505 message = _('There has been an error opening your device.')
2506 self.notification(message, title)
2507 return
2509 if gl.config.device_type == 'ipod':
2510 #update played episodes and delete if requested
2511 for channel in self.channels:
2512 if channel.sync_to_devices:
2513 allepisodes = [ episode for episode in channel.get_all_episodes() if episode.was_downloaded(and_exists=True) ]
2514 device.update_played_or_delete(channel, allepisodes, gl.config.ipod_delete_played_from_db)
2516 if gl.config.ipod_purge_old_episodes:
2517 device.purge()
2519 sync_all_episodes = not bool(episodes)
2521 if episodes is None:
2522 episodes = self.get_all_episodes()
2524 # make sure we have enough space on the device
2525 can_sync = True
2526 total_size = 0
2527 free_space = max(device.get_free_space(), 0)
2528 for episode in episodes:
2529 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
2530 filename = episode.local_filename(create=False)
2531 if filename is not None:
2532 total_size += util.calculate_size(str(filename))
2534 if total_size > free_space:
2535 title = _('Not enough space left on device')
2536 message = _('You need to free up %s.\nDo you want to continue?') % (util.format_filesize(total_size-free_space),)
2537 can_sync = self.show_confirmation(message, title)
2539 if self.tray_icon:
2540 self.tray_icon.set_synchronisation_device(device)
2542 if can_sync:
2543 gPodderSync(device=device, gPodder=self)
2544 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2545 else:
2546 device.close()
2548 # The sync process might have updated the status of episodes,
2549 # therefore persist the database here to avoid losing data
2550 db.commit()
2552 def sync_to_ipod_completed(self, device, successful_sync):
2553 device.unregister( 'post-done', self.sync_to_ipod_completed )
2555 if self.tray_icon:
2556 self.tray_icon.release_synchronisation_device()
2558 if not successful_sync:
2559 title = _('Error closing device')
2560 message = _('There has been an error closing your device.')
2561 self.notification(message, title)
2563 # update model for played state updates after sync
2564 util.idle_add(self.updateComboBox)
2566 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2567 if sync_all_episodes:
2568 device.add_tracks(episodes)
2569 # 'only_sync_not_played' must be used or else all the played
2570 # tracks will be copied then immediately deleted
2571 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2572 self.ipod_delete_played(device)
2573 else:
2574 device.add_tracks(episodes, force_played=True)
2575 device.close()
2576 self.update_selected_episode_list_icons()
2578 def ipod_cleanup_callback(self, device, tracks):
2579 title = _('Delete podcasts from device?')
2580 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?')
2581 if len(tracks) > 0 and self.show_confirmation(message, title):
2582 gPodderSync(device=device, gPodder=self)
2583 Thread(target=self.ipod_cleanup_thread, args=[device, tracks]).start()
2585 def ipod_cleanup_thread(self, device, tracks):
2586 device.remove_tracks(tracks)
2588 if not device.close():
2589 title = _('Error closing device')
2590 message = _('There has been an error closing your device.')
2591 gobject.idle_add(self.show_message, message, title)
2593 def on_cleanup_ipod_activate(self, widget, *args):
2594 columns = (
2595 ('title', None, None, _('Episode')),
2596 ('podcast', None, None, _('Podcast')),
2597 ('filesize', None, None, _('Size')),
2598 ('modified', 'modified_sort', gobject.TYPE_INT, _('Copied')),
2599 ('playcount', None, None, _('Play count')),
2600 ('released', None, None, _('Released')),
2603 device = sync.open_device(gl.config)
2605 if device is None:
2606 title = _('No device configured')
2607 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2608 self.show_message(message, title)
2609 return
2611 if not device.open():
2612 title = _('Cannot open device')
2613 message = _('There has been an error opening your device.')
2614 self.show_message(message, title)
2615 return
2617 tracks = device.get_all_tracks()
2618 if len(tracks) > 0:
2619 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2620 wanted_columns = []
2621 for key, sort_name, sort_type, caption in columns:
2622 want_this_column = False
2623 for track in tracks:
2624 if getattr(track, key) is not None:
2625 want_this_column = True
2626 break
2628 if want_this_column:
2629 wanted_columns.append((key, sort_name, sort_type, caption))
2630 title = _('Remove podcasts from device')
2631 instructions = _('Select the podcast episodes you want to remove from your device.')
2632 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2633 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2634 else:
2635 title = _('No files on device')
2636 message = _('The devices contains no files to be removed.')
2637 self.show_message(message, title)
2638 device.close()
2640 def on_manage_device_playlist(self, widget):
2641 # make sure gpod is available before even trying to sync
2642 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2643 title = _('Cannot manage iPod playlist')
2644 message = _('This feature is not available for iPods.')
2645 self.notification( message, title )
2646 return
2647 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2648 title = _('Cannot manage MTP device playlist')
2649 message = _('This feature is not available for MTP devices.')
2650 self.notification( message, title )
2651 return
2653 device = sync.open_device(gl.config)
2655 if device is None:
2656 title = _('No device configured')
2657 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2658 self.notification(message, title)
2659 return
2661 if not device.open():
2662 title = _('Cannot open device')
2663 message = _('There has been an error opening your device.')
2664 self.notification(message, title)
2665 return
2667 gPodderPlaylist(device=device, gPodder=self)
2668 device.close()
2670 def show_hide_tray_icon(self):
2671 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2672 self.tray_icon = trayicon.GPodderStatusIcon(self, gpodder.icon_file, gl.config)
2673 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2674 self.tray_icon.set_visible(False)
2675 del self.tray_icon
2676 self.tray_icon = None
2678 if gl.config.minimize_to_tray and self.tray_icon:
2679 self.tray_icon.set_visible(self.minimized)
2680 elif self.tray_icon:
2681 self.tray_icon.set_visible(True)
2683 def on_itemShowToolbar_activate(self, widget):
2684 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2686 def on_itemShowDescription_activate(self, widget):
2687 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2689 def update_item_device( self):
2690 if gl.config.device_type != 'none':
2691 self.itemDevice.set_visible(True)
2692 self.itemDevice.label = self.get_device_name()
2693 else:
2694 self.itemDevice.set_visible(False)
2696 def properties_closed( self):
2697 self.show_hide_tray_icon()
2698 self.update_item_device()
2699 self.updateComboBox()
2701 def on_itemPreferences_activate(self, widget, *args):
2702 if gpodder.interface == gpodder.GUI:
2703 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2704 else:
2705 gPodderMaemoPreferences()
2707 def on_itemDependencies_activate(self, widget):
2708 gPodderDependencyManager()
2710 def on_add_new_google_search(self, widget, *args):
2711 def add_google_video_search(query):
2712 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2714 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2716 def on_upgrade_from_videocenter(self, widget):
2717 from gpodder import nokiavideocenter
2718 vc = nokiavideocenter.UpgradeFromVideocenter()
2719 if vc.db2opml():
2720 gPodderOpmlLister(custom_title=_('Import podcasts from Video Center'), hide_url_entry=True).get_channels_from_url(vc.opmlfile, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2721 else:
2722 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2724 def require_my_gpodder_authentication(self):
2725 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2726 success, authentication = self.UsernamePasswordDialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username=gl.config.my_gpodder_username, password=gl.config.my_gpodder_password, username_prompt=_('E-Mail Address'), register_callback=lambda: util.open_website('http://my.gpodder.org/register'))
2727 if success and authentication[0] and authentication[1]:
2728 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2729 return True
2730 else:
2731 return False
2733 return True
2735 def my_gpodder_offer_autoupload(self):
2736 if not gl.config.my_gpodder_autoupload:
2737 if self.show_confirmation(_('gPodder can automatically upload your subscription list to my.gpodder.org when you close it. Do you want to enable this feature?'), _('Upload subscriptions on quit')):
2738 gl.config.my_gpodder_autoupload = True
2740 def on_download_from_mygpo(self, widget):
2741 if self.require_my_gpodder_authentication():
2742 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2743 opml_data = client.download_subscriptions()
2744 if len(opml_data) > 0:
2745 fp = open(gpodder.subscription_file, 'w')
2746 fp.write(opml_data)
2747 fp.close()
2748 (added, skipped) = (0, 0)
2749 i = opml.Importer(gpodder.subscription_file)
2750 for item in i.items:
2751 url = item['url']
2752 if url not in (c.url for c in self.channels):
2753 self.add_new_channel(url, ask_download_new=False, block=True)
2754 added += 1
2755 else:
2756 log('Already added: %s', url, sender=self)
2757 skipped += 1
2758 self.updateComboBox()
2759 if added > 0:
2760 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2761 elif widget is not None:
2762 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2763 self.my_gpodder_offer_autoupload()
2764 else:
2765 gl.config.my_gpodder_password = ''
2766 self.on_download_from_mygpo(widget)
2767 else:
2768 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2770 def on_upload_to_mygpo(self, widget):
2771 if self.require_my_gpodder_authentication():
2772 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2773 self.save_channels_opml()
2774 success, messages = client.upload_subscriptions(gpodder.subscription_file)
2775 if widget is not None:
2776 self.show_message('\n'.join(messages), _('Results of upload'))
2777 if not success:
2778 gl.config.my_gpodder_password = ''
2779 self.on_upload_to_mygpo(widget)
2780 else:
2781 self.my_gpodder_offer_autoupload()
2782 elif not success:
2783 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2784 elif widget is not None:
2785 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2787 def on_itemAddChannel_activate(self, widget, *args):
2788 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2790 def on_itemEditChannel_activate(self, widget, *args):
2791 if self.active_channel is None:
2792 title = _('No podcast selected')
2793 message = _('Please select a podcast in the podcasts list to edit.')
2794 self.show_message( message, title)
2795 return
2797 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True))
2799 def on_itemRemoveChannel_activate(self, widget, *args):
2800 try:
2801 if gpodder.interface == gpodder.GUI:
2802 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2803 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2804 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2806 title = _('Remove podcast and episodes?')
2807 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2809 dialog.set_title(title)
2810 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2812 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2813 dialog.vbox.pack_start(cb_ask)
2814 cb_ask.show_all()
2815 affirmative = gtk.RESPONSE_YES
2816 elif gpodder.interface == gpodder.MAEMO:
2817 cb_ask = gtk.CheckButton('') # dummy check button
2818 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2819 affirmative = gtk.RESPONSE_OK
2821 result = dialog.run()
2822 dialog.destroy()
2824 if result == affirmative:
2825 # delete downloaded episodes only if checkbox is unchecked
2826 if cb_ask.get_active() == False:
2827 self.active_channel.remove_downloaded()
2828 else:
2829 log('Not removing downloaded episodes', sender=self)
2831 # Clean up downloads and download directories
2832 gl.clean_up_downloads()
2834 # cancel any active downloads from this channel
2835 for episode in self.active_channel.get_all_episodes():
2836 self.download_status_manager.cancel_by_url(episode.url)
2838 # get the URL of the podcast we want to select next
2839 position = self.channels.index(self.active_channel)
2840 if position == len(self.channels)-1:
2841 # this is the last podcast, so select the URL
2842 # of the item before this one (i.e. the "new last")
2843 select_url = self.channels[position-1].url
2844 else:
2845 # there is a podcast after the deleted one, so
2846 # we simply select the one that comes after it
2847 select_url = self.channels[position+1].url
2849 # Remove the channel
2850 self.active_channel.delete()
2851 self.channels.remove(self.active_channel)
2852 self.channel_list_changed = True
2853 self.save_channels_opml()
2855 # Re-load the channels and select the desired new channel
2856 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2857 except:
2858 log('There has been an error removing the channel.', traceback=True, sender=self)
2859 self.update_podcasts_tab()
2861 def get_opml_filter(self):
2862 filter = gtk.FileFilter()
2863 filter.add_pattern('*.opml')
2864 filter.add_pattern('*.xml')
2865 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2866 return filter
2868 def on_item_import_from_file_activate(self, widget, filename=None):
2869 if filename is None:
2870 if gpodder.interface == gpodder.GUI:
2871 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2872 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2873 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2874 elif gpodder.interface == gpodder.MAEMO:
2875 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2876 dlg.set_filter(self.get_opml_filter())
2877 response = dlg.run()
2878 filename = None
2879 if response == gtk.RESPONSE_OK:
2880 filename = dlg.get_filename()
2881 dlg.destroy()
2883 if filename is not None:
2884 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))
2886 def on_itemExportChannels_activate(self, widget, *args):
2887 if not self.channels:
2888 title = _('Nothing to export')
2889 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2890 self.show_message( message, title)
2891 return
2893 if gpodder.interface == gpodder.GUI:
2894 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2895 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2896 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2897 elif gpodder.interface == gpodder.MAEMO:
2898 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2899 dlg.set_filter(self.get_opml_filter())
2900 response = dlg.run()
2901 if response == gtk.RESPONSE_OK:
2902 filename = dlg.get_filename()
2903 dlg.destroy()
2904 exporter = opml.Exporter( filename)
2905 if exporter.write(self.channels):
2906 if len(self.channels) == 1:
2907 title = _('One subscription exported')
2908 else:
2909 title = _('%d subscriptions exported') % len(self.channels)
2910 self.show_message(_('Your podcast list has been successfully exported.'), title)
2911 else:
2912 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2913 else:
2914 dlg.destroy()
2916 def on_itemImportChannels_activate(self, widget, *args):
2917 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))
2919 def on_homepage_activate(self, widget, *args):
2920 util.open_website(gpodder.__url__)
2922 def on_wiki_activate(self, widget, *args):
2923 util.open_website('http://wiki.gpodder.org/')
2925 def on_bug_tracker_activate(self, widget, *args):
2926 if gpodder.interface == gpodder.MAEMO:
2927 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2928 else:
2929 util.open_website('http://bugs.gpodder.org/')
2931 def on_shop_activate(self, widget, *args):
2932 util.open_website('http://gpodder.org/shop')
2934 def on_wishlist_activate(self, widget, *args):
2935 util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2937 def on_itemAbout_activate(self, widget, *args):
2938 dlg = gtk.AboutDialog()
2939 dlg.set_name('gPodder')
2940 dlg.set_version(gpodder.__version__)
2941 dlg.set_copyright(gpodder.__copyright__)
2942 dlg.set_website(gpodder.__url__)
2943 dlg.set_translator_credits( _('translator-credits'))
2944 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2946 if gpodder.interface == gpodder.GUI:
2947 # For the "GUI" version, we add some more
2948 # items to the about dialog (credits and logo)
2949 dlg.set_authors(app_authors)
2950 try:
2951 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
2952 except:
2953 dlg.set_logo_icon_name('gpodder')
2955 dlg.run()
2957 def on_wNotebook_switch_page(self, widget, *args):
2958 page_num = args[1]
2959 if gpodder.interface == gpodder.MAEMO:
2960 self.tool_downloads.set_active(page_num == 1)
2961 page = self.wNotebook.get_nth_page(page_num)
2962 tab_label = self.wNotebook.get_tab_label(page).get_text()
2963 if page_num == 0 and self.active_channel is not None:
2964 self.set_title(self.active_channel.title)
2965 else:
2966 self.set_title(tab_label)
2967 if page_num == 0:
2968 self.play_or_download()
2969 self.menuChannels.set_sensitive(True)
2970 self.menuSubscriptions.set_sensitive(True)
2971 # The message area in the downloads tab should be hidden
2972 # when the user switches away from the downloads tab
2973 if self.message_area is not None:
2974 self.message_area.hide()
2975 self.message_area = None
2976 else:
2977 self.menuChannels.set_sensitive(False)
2978 self.menuSubscriptions.set_sensitive(False)
2979 self.toolDownload.set_sensitive( False)
2980 self.toolPlay.set_sensitive( False)
2981 self.toolTransfer.set_sensitive( False)
2982 self.toolCancel.set_sensitive( False)#services.download_status_manager.has_items())
2984 def on_treeChannels_row_activated(self, widget, path, *args):
2985 # double-click action of the podcast list or enter
2986 self.treeChannels.set_cursor(path)
2988 def on_treeChannels_cursor_changed(self, widget, *args):
2989 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2991 if model is not None and iter is not None:
2992 old_active_channel = self.active_channel
2993 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
2995 if self.active_channel == old_active_channel:
2996 return
2998 if gpodder.interface == gpodder.MAEMO:
2999 self.set_title(self.active_channel.title)
3000 self.itemEditChannel.set_visible(True)
3001 self.itemRemoveChannel.set_visible(True)
3002 self.channel_toggle_lock.set_visible(True)
3003 if self.active_channel.channel_is_locked:
3004 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
3005 else:
3006 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
3008 else:
3009 self.active_channel = None
3010 self.itemEditChannel.set_visible(False)
3011 self.itemRemoveChannel.set_visible(False)
3012 self.channel_toggle_lock.set_visible(False)
3014 self.updateTreeView()
3016 def on_entryAddChannel_changed(self, widget, *args):
3017 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
3018 self.btnAddChannel.set_sensitive( active)
3020 def on_btnAddChannel_clicked(self, widget, *args):
3021 url = self.entryAddChannel.get_text()
3022 self.entryAddChannel.set_text('')
3023 self.add_new_channel( url)
3025 def on_btnEditChannel_clicked(self, widget, *args):
3026 self.on_itemEditChannel_activate( widget, args)
3028 def get_selected_episodes(self):
3029 """Get a list of selected episodes from treeAvailable"""
3030 selection = self.treeAvailable.get_selection()
3031 model, paths = selection.get_selected_rows()
3033 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3034 return episodes
3036 def on_transfer_selected_episodes(self, widget):
3037 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3039 def on_playback_selected_episodes(self, widget):
3040 self.playback_episodes(self.get_selected_episodes())
3042 def on_shownotes_selected_episodes(self, widget):
3043 episodes = self.get_selected_episodes()
3044 if episodes:
3045 episode = episodes.pop(0)
3046 self.show_episode_shownotes(episode)
3047 else:
3048 self.show_message(_('No episode selected'), _('Please select an episode'))
3050 def on_download_selected_episodes(self, widget):
3051 episodes = self.get_selected_episodes()
3052 self.download_episode_list(episodes)
3053 self.update_episode_list_icons([episode.url for episode in episodes])
3054 self.play_or_download()
3056 def on_treeAvailable_row_activated(self, widget, path, view_column):
3057 """Double-click/enter action handler for treeAvailable"""
3058 # We should only have one one selected as it was double clicked!
3059 e = self.get_selected_episodes()[0]
3061 if (gl.config.double_click_episode_action == 'download'):
3062 # If the episode has already been downloaded and exists then play it
3063 if e.was_downloaded(and_exists=True):
3064 self.playback_episodes(self.get_selected_episodes())
3065 # else download it if it is not already downloading
3066 elif not self.episode_is_downloading(e):
3067 self.download_episode_list([e])
3068 self.update_episode_list_icons([e.url])
3069 self.play_or_download()
3070 elif (gl.config.double_click_episode_action == 'stream'):
3071 # If we happen to have downloaded this episode simple play it
3072 if e.was_downloaded(and_exists=True):
3073 self.playback_episodes(self.get_selected_episodes())
3074 # else if streaming is possible stream it
3075 elif gl.streaming_possible():
3076 self.playback_episodes(self.get_selected_episodes())
3077 else:
3078 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3079 self.show_message( _('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'))
3080 else:
3081 # default action is to display show notes
3082 self.on_shownotes_selected_episodes(widget)
3084 def show_episode_shownotes(self, episode):
3085 play_callback = lambda: self.playback_episodes([episode])
3086 def download_callback():
3087 self.download_episode_list([episode])
3088 self.play_or_download()
3089 if self.gpodder_episode_window is None:
3090 log('First-time use of episode window --- creating', sender=self)
3091 self.gpodder_episode_window = gPodderEpisode(\
3092 download_status_manager=self.download_status_manager, \
3093 episode_is_downloading=self.episode_is_downloading)
3094 self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
3096 def on_treeAvailable_button_release_event(self, widget, *args):
3097 self.play_or_download()
3099 def auto_update_procedure(self, first_run=False):
3100 log('auto_update_procedure() got called', sender=self)
3101 if not first_run and gl.config.auto_update_feeds and self.minimized:
3102 self.update_feed_cache(force_update=True)
3104 next_update = 60*1000*gl.config.auto_update_frequency
3105 gobject.timeout_add(next_update, self.auto_update_procedure)
3107 def on_treeDownloads_row_activated(self, widget, *args):
3108 if self.wNotebook.get_current_page() == 0:
3109 # Use the available podcasts treeview + model
3110 selection = self.treeAvailable.get_selection()
3111 (model, paths) = selection.get_selected_rows()
3112 urls = [model.get_value(model.get_iter(path), 0) for path in paths]
3113 selected_tasks = [task for task in self.download_tasks_seen if task.url in urls]
3114 for task in selected_tasks:
3115 task.status = task.CANCELLED
3116 self.update_selected_episode_list_icons()
3117 self.play_or_download()
3118 return
3120 # Use the standard way of working on the treeview
3121 selection = self.treeDownloads.get_selection()
3122 (model, paths) = selection.get_selected_rows()
3123 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3125 for tree_row_reference, task in selected_tasks:
3126 if task.status in (task.DOWNLOADING, task.QUEUED):
3127 task.status = task.PAUSED
3128 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3129 self.download_queue_manager.add_task(task)
3130 self.enable_download_list_update()
3131 elif task.status == task.DONE:
3132 model.remove(model.get_iter(tree_row_reference.get_path()))
3134 self.play_or_download()
3136 # Update the tab title and downloads list
3137 self.update_downloads_list()
3139 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
3140 self.on_treeDownloads_row_activated( widget, None)
3142 def on_btnCancelAll_clicked(self, widget, *args):
3143 self.treeDownloads.get_selection().select_all()
3144 self.on_treeDownloads_row_activated( self.toolCancel, None)
3145 self.treeDownloads.get_selection().unselect_all()
3147 # Update the tab title and downloads list
3148 self.update_downloads_list()
3150 def on_btnDownloadedDelete_clicked(self, widget, *args):
3151 if self.active_channel is None:
3152 return
3154 if self.wNotebook.get_current_page() == 1:
3155 # Downloads tab visible - no action!
3156 return
3158 episodes = self.get_selected_episodes()
3160 if not episodes:
3161 log('Nothing selected - will not remove any downloaded episode.')
3162 return
3164 if len(episodes) == 1:
3165 episode = episodes[0]
3166 if episode.is_locked:
3167 title = _('%s is locked') % saxutils.escape(episode.title)
3168 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
3169 self.notification(message, title)
3170 return
3172 title = _('Remove %s?') % saxutils.escape(episode.title)
3173 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.")
3174 else:
3175 title = _('Remove %d episodes?') % len(episodes)
3176 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.')
3178 locked_count = sum(int(e.is_locked) for e in episodes if e.is_locked is not None)
3180 if len(episodes) == locked_count:
3181 title = _('Episodes are locked')
3182 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3183 self.notification(message, title)
3184 return
3185 elif locked_count > 0:
3186 title = _('Remove %d out of %d episodes?') % (len(episodes)-locked_count, len(episodes))
3187 message = _('The selection contains locked episodes that will not be deleted. If you want to listen to the deleted episodes, you will have to re-download them.')
3189 # if user confirms deletion, let's remove some stuff ;)
3190 if self.show_confirmation(message, title):
3191 for episode in episodes:
3192 if not episode.is_locked:
3193 episode.delete_from_disk()
3194 self.updateComboBox(only_selected_channel=True)
3196 # only delete partial files if we do not have any downloads in progress
3197 gl.clean_up_downloads(False)
3198 self.update_selected_episode_list_icons()
3199 self.play_or_download()
3201 def on_key_press(self, widget, event):
3202 # Allow tab switching with Ctrl + PgUp/PgDown
3203 if event.state & gtk.gdk.CONTROL_MASK:
3204 if event.keyval == gtk.keysyms.Page_Up:
3205 self.wNotebook.prev_page()
3206 return True
3207 elif event.keyval == gtk.keysyms.Page_Down:
3208 self.wNotebook.next_page()
3209 return True
3211 # After this code we only handle Maemo hardware keys,
3212 # so if we are not a Maemo app, we don't do anything
3213 if gpodder.interface != gpodder.MAEMO:
3214 return False
3216 if event.keyval == gtk.keysyms.F6:
3217 if self.fullscreen:
3218 self.window.unfullscreen()
3219 else:
3220 self.window.fullscreen()
3221 if event.keyval == gtk.keysyms.Escape:
3222 new_visibility = not self.vboxChannelNavigator.get_property('visible')
3223 self.vboxChannelNavigator.set_property('visible', new_visibility)
3224 self.column_size.set_visible(not new_visibility)
3225 self.column_released.set_visible(not new_visibility)
3227 diff = 0
3228 if event.keyval == gtk.keysyms.F7: #plus
3229 diff = 1
3230 elif event.keyval == gtk.keysyms.F8: #minus
3231 diff = -1
3233 if diff != 0 and not self.currently_updating:
3234 selection = self.treeChannels.get_selection()
3235 (model, iter) = selection.get_selected()
3236 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3237 selection.select_path(new_path)
3238 self.treeChannels.set_cursor(new_path)
3239 return True
3241 return False
3243 def window_state_event(self, widget, event):
3244 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
3245 self.fullscreen = True
3246 else:
3247 self.fullscreen = False
3249 old_minimized = self.minimized
3251 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
3252 if gpodder.interface == gpodder.MAEMO:
3253 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
3255 if old_minimized != self.minimized and self.tray_icon:
3256 self.gPodder.set_skip_taskbar_hint(self.minimized)
3257 elif not self.tray_icon:
3258 self.gPodder.set_skip_taskbar_hint(False)
3260 if gl.config.minimize_to_tray and self.tray_icon:
3261 self.tray_icon.set_visible(self.minimized)
3263 def uniconify_main_window(self):
3264 if self.minimized:
3265 self.gPodder.present()
3267 def iconify_main_window(self):
3268 if not self.minimized:
3269 self.gPodder.iconify()
3271 def update_podcasts_tab(self):
3272 if len(self.channels):
3273 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3274 else:
3275 self.label2.set_text(_('Podcasts'))
3277 @dbus.service.method(gpodder.dbus_interface)
3278 def show_gui_window(self):
3279 self.gPodder.present()
3281 class gPodderChannel(BuilderWidget):
3282 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
3284 def new(self):
3285 global WEB_BROWSER_ICON
3286 self.changed = False
3287 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
3288 self.gPodderChannel.set_title( self.channel.title)
3289 self.entryTitle.set_text( self.channel.title)
3290 self.labelURL.set_text(self.channel.url)
3292 self.LabelDownloadTo.set_text( self.channel.save_dir)
3293 self.LabelWebsite.set_text( self.channel.link)
3295 self.cbNoSync.set_active( not self.channel.sync_to_devices)
3296 self.musicPlaylist.set_text(self.channel.device_playlist_name)
3297 if self.channel.username:
3298 self.FeedUsername.set_text( self.channel.username)
3299 if self.channel.password:
3300 self.FeedPassword.set_text( self.channel.password)
3302 services.cover_downloader.register('cover-available', self.cover_download_finished)
3303 services.cover_downloader.request_cover(self.channel)
3305 # Hide the website button if we don't have a valid URL
3306 if not self.channel.link:
3307 self.btn_website.hide_all()
3309 b = gtk.TextBuffer()
3310 b.set_text( self.channel.description)
3311 self.channel_description.set_buffer( b)
3313 #Add Drag and Drop Support
3314 flags = gtk.DEST_DEFAULT_ALL
3315 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
3316 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
3317 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
3318 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
3320 def on_btn_website_clicked(self, widget):
3321 util.open_website(self.channel.link)
3323 def on_btnDownloadCover_clicked(self, widget):
3324 if gpodder.interface == gpodder.GUI:
3325 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3326 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3327 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3328 elif gpodder.interface == gpodder.MAEMO:
3329 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
3331 if dlg.run() == gtk.RESPONSE_OK:
3332 url = dlg.get_uri()
3333 services.cover_downloader.replace_cover(self.channel, url)
3335 dlg.destroy()
3337 def on_btnClearCover_clicked(self, widget):
3338 services.cover_downloader.replace_cover(self.channel)
3340 def cover_download_finished(self, channel_url, pixbuf):
3341 if pixbuf is not None:
3342 self.imgCover.set_from_pixbuf(pixbuf)
3343 self.gPodderChannel.show()
3345 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
3346 files = sel.data.strip().split('\n')
3347 if len(files) != 1:
3348 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
3349 return
3351 file = files[0]
3353 if file.startswith('file://') or file.startswith('http://'):
3354 services.cover_downloader.replace_cover(self.channel, file)
3355 return
3357 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
3359 def on_gPodderChannel_destroy(self, widget, *args):
3360 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
3362 def on_btnOK_clicked(self, widget, *args):
3363 self.channel.sync_to_devices = not self.cbNoSync.get_active()
3364 self.channel.device_playlist_name = self.musicPlaylist.get_text()
3365 self.channel.set_custom_title(self.entryTitle.get_text())
3366 self.channel.username = self.FeedUsername.get_text().strip()
3367 self.channel.password = self.FeedPassword.get_text()
3368 self.channel.save()
3370 self.gPodderChannel.destroy()
3371 self.callback_closed()
3373 class gPodderAddPodcastDialog(BuilderWidget):
3374 finger_friendly_widgets = ['btn_close', 'btn_add']
3376 def new(self):
3377 if not hasattr(self, 'url_callback'):
3378 log('No url callback set', sender=self)
3379 self.url_callback = None
3380 if hasattr(self, 'custom_label'):
3381 self.label_add.set_text(self.custom_label)
3382 if hasattr(self, 'custom_title'):
3383 self.gPodderAddPodcastDialog.set_title(self.custom_title)
3384 if gpodder.interface == gpodder.MAEMO:
3385 self.entry_url.set_text('http://')
3386 if hasattr(self, 'preset_url'):
3387 self.entry_url.set_text(self.preset_url)
3388 if hasattr(self, 'btn_add_stock_id'):
3389 self.btn_add.set_label(self.btn_add_stock_id)
3390 self.btn_add.set_use_stock(True)
3391 self.entry_url.connect('activate', self.on_entry_url_activate)
3392 self.gPodderAddPodcastDialog.show()
3394 def on_btn_close_clicked(self, widget):
3395 self.gPodderAddPodcastDialog.destroy()
3397 def on_btn_paste_clicked(self, widget):
3398 clipboard = gtk.Clipboard()
3399 clipboard.request_text(self.receive_clipboard_text)
3401 def receive_clipboard_text(self, clipboard, text, data=None):
3402 if text is not None:
3403 self.entry_url.set_text(text)
3404 else:
3405 self.show_message(_('Nothing to paste.'), _('Clipboard is empty'))
3407 def on_entry_url_changed(self, widget):
3408 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
3410 def on_entry_url_activate(self, widget):
3411 self.on_btn_add_clicked(widget)
3413 def on_btn_add_clicked(self, widget):
3414 url = self.entry_url.get_text()
3415 self.on_btn_close_clicked(widget)
3416 if self.url_callback is not None:
3417 self.url_callback(url)
3420 class gPodderMaemoPreferences(BuilderWidget):
3421 finger_friendly_widgets = ['btn_close', 'btn_advanced']
3422 audio_players = [
3423 ('default', 'Media Player'),
3424 ('panucci', 'Panucci'),
3426 video_players = [
3427 ('default', 'Media Player'),
3428 ('mplayer', 'MPlayer'),
3431 def new(self):
3432 gl.config.connect_gtk_togglebutton('display_tray_icon', self.check_show_status_icon)
3433 gl.config.connect_gtk_togglebutton('on_quit_ask', self.check_ask_on_quit)
3434 gl.config.connect_gtk_togglebutton('maemo_enable_gestures', self.check_enable_gestures)
3436 for item in self.audio_players:
3437 command, caption = item
3438 if util.find_command(command) is None and command != 'default':
3439 self.audio_players.remove(item)
3441 for item in self.video_players:
3442 command, caption = item
3443 if util.find_command(command) is None and command != 'default':
3444 self.video_players.remove(item)
3446 # Set up the audio player combobox
3447 found = False
3448 self.userconfigured_player = None
3449 for id, audio_player in enumerate(self.audio_players):
3450 command, caption = audio_player
3451 self.combo_player_model.append([caption])
3452 if gl.config.player == command:
3453 self.combo_player.set_active(id)
3454 found = True
3455 if not found:
3456 self.combo_player_model.append(['User-configured (%s)' % gl.config.player])
3457 self.combo_player.set_active(len(self.combo_player_model)-1)
3458 self.userconfigured_player = gl.config.player
3460 # Set up the video player combobox
3461 found = False
3462 self.userconfigured_videoplayer = None
3463 for id, video_player in enumerate(self.video_players):
3464 command, caption = video_player
3465 self.combo_videoplayer_model.append([caption])
3466 if gl.config.videoplayer == command:
3467 self.combo_videoplayer.set_active(id)
3468 found = True
3469 if not found:
3470 self.combo_videoplayer_model.append(['User-configured (%s)' % gl.config.videoplayer])
3471 self.combo_videoplayer.set_active(len(self.combo_videoplayer_model)-1)
3472 self.userconfigured_videoplayer = gl.config.videoplayer
3474 self.gPodderMaemoPreferences.show()
3476 def on_combo_player_changed(self, combobox):
3477 index = combobox.get_active()
3478 if index < len(self.audio_players):
3479 gl.config.player = self.audio_players[index][0]
3480 elif self.userconfigured_player is not None:
3481 gl.config.player = self.userconfigured_player
3483 def on_combo_videoplayer_changed(self, combobox):
3484 index = combobox.get_active()
3485 if index < len(self.video_players):
3486 gl.config.videoplayer = self.video_players[index][0]
3487 elif self.userconfigured_videoplayer is not None:
3488 gl.config.videoplayer = self.userconfigured_videoplayer
3490 def on_btn_advanced_clicked(self, widget):
3491 self.gPodderMaemoPreferences.destroy()
3492 gPodderConfigEditor()
3494 def on_btn_close_clicked(self, widget):
3495 self.gPodderMaemoPreferences.destroy()
3498 class gPodderProperties(BuilderWidget):
3499 def new(self):
3500 if not hasattr( self, 'callback_finished'):
3501 self.callback_finished = None
3503 if gpodder.interface == gpodder.MAEMO:
3504 self.table5.hide_all() # player
3505 self.gPodderProperties.fullscreen()
3507 gl.config.connect_gtk_editable( 'player', self.openApp)
3508 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
3509 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
3510 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
3511 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
3512 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
3513 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
3514 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
3515 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
3516 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
3517 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
3518 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
3519 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
3520 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
3521 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
3522 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
3523 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
3524 gl.config.connect_gtk_togglebutton('ipod_delete_played_from_db', self.ipod_delete_played_from_db)
3525 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
3526 gl.config.connect_gtk_togglebutton('disable_pre_sync_conversion', self.player_supports_ogg)
3528 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
3529 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
3531 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
3533 self.iPodMountpoint.set_label( gl.config.ipod_mount)
3534 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
3535 self.chooserDownloadTo.set_current_folder(gl.config.download_dir)
3537 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
3538 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
3540 # device type
3541 self.comboboxDeviceType.set_active( 0)
3542 if gl.config.device_type == 'ipod':
3543 self.comboboxDeviceType.set_active( 1)
3544 elif gl.config.device_type == 'filesystem':
3545 self.comboboxDeviceType.set_active( 2)
3546 elif gl.config.device_type == 'mtp':
3547 self.comboboxDeviceType.set_active( 3)
3549 # setup cell renderers
3550 cellrenderer = gtk.CellRendererPixbuf()
3551 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
3552 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3553 cellrenderer = gtk.CellRendererText()
3554 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
3555 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3557 cellrenderer = gtk.CellRendererPixbuf()
3558 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
3559 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3560 cellrenderer = gtk.CellRendererText()
3561 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
3562 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3564 if not hasattr(self, 'user_apps_reader'):
3565 self.user_apps_reader = UserAppsReader(['audio', 'video'])
3567 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
3568 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
3570 if gpodder.interface == gpodder.GUI:
3571 self.user_apps_reader.read()
3573 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
3574 index = self.find_active_audio_app()
3575 self.comboAudioPlayerApp.set_active(index)
3576 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
3577 index = self.find_active_video_app()
3578 self.comboVideoPlayerApp.set_active(index)
3580 # auto download option
3581 self.comboboxAutoDownload.set_active( 0)
3582 if gl.config.auto_download == 'minimized':
3583 self.comboboxAutoDownload.set_active( 1)
3584 elif gl.config.auto_download == 'always':
3585 self.comboboxAutoDownload.set_active( 2)
3587 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
3589 def is_row_separator(self, model, iter):
3590 return model.get_value(iter, 0) == ''
3592 def update_mountpoint( self, ipod):
3593 if ipod is None or ipod.mount_point is None:
3594 self.iPodMountpoint.set_label( '')
3595 else:
3596 self.iPodMountpoint.set_label( ipod.mount_point)
3598 def find_active_audio_app(self):
3599 index_custom = -1
3600 model = self.comboAudioPlayerApp.get_model()
3601 iter = model.get_iter_first()
3602 index = 0
3603 while iter is not None:
3604 command = model.get_value(iter, 1)
3605 if command == self.openApp.get_text():
3606 return index
3607 if index_custom < 0 and command == '':
3608 index_custom = index
3609 iter = model.iter_next(iter)
3610 index += 1
3611 # return index of custom command or first item
3612 return max(0, index_custom)
3614 def find_active_video_app( self):
3615 index_custom = -1
3616 model = self.comboVideoPlayerApp.get_model()
3617 iter = model.get_iter_first()
3618 index = 0
3619 while iter is not None:
3620 command = model.get_value(iter, 1)
3621 if command == self.openVideoApp.get_text():
3622 return index
3623 if index_custom < 0 and command == '':
3624 index_custom = index
3625 iter = model.iter_next(iter)
3626 index += 1
3627 # return index of custom command or first item
3628 return max(0, index_custom)
3630 def on_auto_update_feeds_toggled( self, widget, *args):
3631 self.auto_update_frequency.set_sensitive(widget.get_active())
3633 def on_display_tray_icon_toggled( self, widget, *args):
3634 self.enable_notifications.set_sensitive(widget.get_active())
3635 self.minimize_to_tray.set_sensitive(widget.get_active())
3637 def on_cbCustomSyncName_toggled( self, widget, *args):
3638 self.entryCustomSyncName.set_sensitive( widget.get_active())
3640 def on_only_sync_not_played_toggled( self, widget, *args):
3641 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3642 if not widget.get_active():
3643 self.delete_episodes_marked_played.set_active(False)
3645 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3646 if widget.get_active() and self.only_sync_not_played.get_active():
3647 self.on_sync_leave.set_active(True)
3648 self.on_sync_delete.set_sensitive(not widget.get_active())
3649 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3651 def on_btnCustomSyncNameHelp_clicked( self, widget):
3652 examples = [
3653 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3654 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3655 '<i>{episode.published}</i> -&gt; <b>20070908</b> (for 08.09.2007)',
3656 '<i>{episode.pubtime}</i> -&gt; <b>1344</b> (for 13:44)',
3657 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3660 info = [
3661 _('You can specify a custom format string for the file names on your MP3 player here.'),
3662 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3663 '\n'.join( [ ' %s' % s for s in examples ])
3666 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3668 def on_gPodderProperties_destroy(self, widget, *args):
3669 self.on_btnOK_clicked( widget, *args)
3671 def on_btnConfigEditor_clicked(self, widget, *args):
3672 self.on_btnOK_clicked(widget, *args)
3673 gPodderConfigEditor()
3675 def on_comboAudioPlayerApp_changed(self, widget, *args):
3676 # find out which one
3677 iter = self.comboAudioPlayerApp.get_active_iter()
3678 model = self.comboAudioPlayerApp.get_model()
3679 command = model.get_value( iter, 1)
3680 if command == '':
3681 if self.openApp.get_text() == 'default':
3682 self.openApp.set_text('')
3683 self.openApp.set_sensitive( True)
3684 self.openApp.show()
3685 self.labelCustomCommand.show()
3686 else:
3687 self.openApp.set_text( command)
3688 self.openApp.set_sensitive( False)
3689 self.openApp.hide()
3690 self.labelCustomCommand.hide()
3692 def on_comboVideoPlayerApp_changed(self, widget, *args):
3693 # find out which one
3694 iter = self.comboVideoPlayerApp.get_active_iter()
3695 model = self.comboVideoPlayerApp.get_model()
3696 command = model.get_value(iter, 1)
3697 if command == '':
3698 if self.openVideoApp.get_text() == 'default':
3699 self.openVideoApp.set_text('')
3700 self.openVideoApp.set_sensitive(True)
3701 self.openVideoApp.show()
3702 self.labelCustomVideoCommand.show()
3703 else:
3704 self.openVideoApp.set_text(command)
3705 self.openVideoApp.set_sensitive(False)
3706 self.openVideoApp.hide()
3707 self.labelCustomVideoCommand.hide()
3709 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3710 sens = not self.cbEnvironmentVariables.get_active()
3711 self.httpProxy.set_sensitive( sens)
3713 def on_comboboxDeviceType_changed(self, widget, *args):
3714 active_item = self.comboboxDeviceType.get_active()
3716 # None
3717 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3718 self.imageSyncOptions, self. separatorSyncOptions,
3719 self.on_sync_mark_played, self.on_sync_delete,
3720 self.on_sync_leave, self.label_after_sync,
3721 self.delete_episodes_marked_played,
3722 self.player_supports_ogg )
3724 for widget in sync_widgets:
3725 if active_item == 0:
3726 widget.hide_all()
3727 else:
3728 widget.show_all()
3730 # iPod
3731 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3732 self.ipod_delete_played_from_db)
3734 for widget in ipod_widgets:
3735 if active_item == 1:
3736 widget.show_all()
3737 else:
3738 widget.hide_all()
3740 # filesystem-based MP3 player
3741 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3742 self.cbChannelSubfolder, self.cbCustomSyncName,
3743 self.entryCustomSyncName, self.btnCustomSyncNameHelp,
3744 self.player_supports_ogg )
3746 for widget in fs_widgets:
3747 if active_item == 2:
3748 widget.show_all()
3749 else:
3750 widget.hide_all()
3752 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3753 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3754 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3755 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3756 fs.set_current_folder(self.iPodMountpoint.get_label())
3757 if fs.run() == gtk.RESPONSE_OK:
3758 self.iPodMountpoint.set_label( fs.get_filename())
3759 fs.destroy()
3761 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3762 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3763 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3764 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3765 fs.set_current_folder(self.filesystemMountpoint.get_label())
3766 if fs.run() == gtk.RESPONSE_OK:
3767 self.filesystemMountpoint.set_label( fs.get_filename())
3768 fs.destroy()
3770 def on_btnOK_clicked(self, widget, *args):
3771 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3772 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3774 # FIXME: set gl.config.download_dir to self.chooserDownloadTo.get_filename() and move download folder!
3776 device_type = self.comboboxDeviceType.get_active()
3777 if device_type == 0:
3778 gl.config.device_type = 'none'
3779 elif device_type == 1:
3780 gl.config.device_type = 'ipod'
3781 elif device_type == 2:
3782 gl.config.device_type = 'filesystem'
3783 elif device_type == 3:
3784 gl.config.device_type = 'mtp'
3786 auto_download = self.comboboxAutoDownload.get_active()
3787 if auto_download == 0:
3788 gl.config.auto_download = 'never'
3789 elif auto_download == 1:
3790 gl.config.auto_download = 'minimized'
3791 elif auto_download == 2:
3792 gl.config.auto_download = 'always'
3793 self.gPodderProperties.destroy()
3794 if self.callback_finished:
3795 self.callback_finished()
3798 class gPodderEpisode(BuilderWidget):
3799 finger_friendly_widgets = ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3801 def new(self):
3802 setattr(self, 'episode', None)
3803 setattr(self, 'download_callback', None)
3804 setattr(self, 'play_callback', None)
3805 self.gPodderEpisode.connect('delete-event', self.on_delete_event)
3806 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3807 self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
3808 if gl.config.enable_html_shownotes and \
3809 not gpodder.interface == gpodder.MAEMO:
3810 try:
3811 import gtkhtml2
3812 setattr(self, 'have_gtkhtml2', True)
3813 # Generate a HTML view and remove the textview
3814 setattr(self, 'htmlview', gtkhtml2.View())
3815 self.scrolled_window.remove(self.scrolled_window.get_child())
3816 self.scrolled_window.add(self.htmlview)
3817 self.textview = None
3818 self.htmlview.set_document(gtkhtml2.Document())
3819 self.htmlview.show()
3820 except ImportError:
3821 log('Install gtkhtml2 if you want HTML shownotes', sender=self)
3822 setattr(self, 'have_gtkhtml2', False)
3823 else:
3824 setattr(self, 'have_gtkhtml2', False)
3825 self.gPodderEpisode.connect('key-press-event', self.on_key_press)
3827 def on_key_press(self, widget, event):
3828 if not hasattr(self.scrolled_window, 'get_vscrollbar'):
3829 return
3830 vsb = self.scrolled_window.get_vscrollbar()
3831 vadj = vsb.get_adjustment()
3832 step = vadj.step_increment
3833 if event.keyval in (gtk.keysyms.J, gtk.keysyms.j):
3834 vsb.set_value(vsb.get_value() + step)
3835 elif event.keyval in (gtk.keysyms.K, gtk.keysyms.k):
3836 vsb.set_value(vsb.get_value() - step)
3838 def show(self, episode, download_callback, play_callback):
3839 self.download_progress.set_fraction(0)
3840 self.download_progress.set_text(_('Please wait...'))
3841 self.episode = episode
3842 self.download_callback = download_callback
3843 self.play_callback = play_callback
3845 self.gPodderEpisode.set_title(self.episode.title)
3847 if self.have_gtkhtml2:
3848 import gtkhtml2
3849 d = gtkhtml2.Document()
3850 d.open_stream('text/html')
3851 d.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3852 d.close_stream()
3853 self.htmlview.set_document(d)
3854 else:
3855 b = gtk.TextBuffer()
3856 self.textview.set_buffer(b)
3858 self.hide_show_widgets()
3859 self.gPodderEpisode.show()
3861 # Make sure the window comes up right now:
3862 while gtk.events_pending():
3863 gtk.main_iteration(False)
3865 # Now do the stuff that takes a bit longer...
3866 heading = self.episode.title
3867 subheading = 'from %s' % (self.episode.channel.title)
3868 description = self.episode.description
3869 footer = []
3871 if self.have_gtkhtml2:
3872 import gtkhtml2
3873 d.connect('link-clicked', lambda d, url: util.open_website(url))
3874 def request_url(document, url, stream):
3875 def opendata(url, stream):
3876 fp = urllib2.urlopen(url)
3877 data = fp.read(1024*10)
3878 while data != '':
3879 stream.write(data)
3880 data = fp.read(1024*10)
3881 stream.close()
3882 Thread(target=opendata, args=[url, stream]).start()
3883 d.connect('request-url', request_url)
3884 d.clear()
3885 d.open_stream('text/html')
3886 d.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3887 d.write_stream('<span style="font-size: big; font-weight: bold;">%s</span><br><span style="font-size: small;">%s</span><hr style="border: 1px #eeeeee solid;"><p>' % (saxutils.escape(heading), saxutils.escape(subheading)))
3888 d.write_stream(self.episode.description)
3889 if len(footer):
3890 d.write_stream('<hr style="border: 1px #eeeeee solid;">')
3891 d.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils.escape(f) for f in footer))),))
3892 d.write_stream('</p></body></html>')
3893 d.close_stream()
3894 else:
3895 b.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
3896 b.create_tag('subheading', scale=pango.SCALE_SMALL)
3897 b.create_tag('footer', scale=pango.SCALE_SMALL)
3899 b.insert_with_tags_by_name(b.get_end_iter(), heading, 'heading')
3900 b.insert_at_cursor('\n')
3901 b.insert_with_tags_by_name(b.get_end_iter(), subheading, 'subheading')
3902 b.insert_at_cursor('\n\n')
3903 b.insert(b.get_end_iter(), util.remove_html_tags(description))
3904 if len(footer):
3905 b.insert_at_cursor('\n\n')
3906 b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
3907 b.place_cursor(b.get_start_iter())
3909 def on_cancel(self, widget):
3910 self.download_status_manager.cancel_by_url(self.episode.url)
3912 def on_delete_event(self, widget, event):
3913 # Avoid destroying the dialog, simply hide
3914 self.on_close(widget)
3915 return True
3917 def on_close(self, widget):
3918 self.episode = None
3919 if self.have_gtkhtml2:
3920 import gtkhtml2
3921 self.htmlview.set_document(gtkhtml2.Document())
3922 else:
3923 self.textview.get_buffer().set_text('')
3924 self.gPodderEpisode.hide()
3926 def download_status_changed(self, episode_urls):
3927 # Reload the episode from the database, so a newly-set local_filename
3928 # as a result of a download gets updated in the episode object
3929 self.episode.reload_from_db()
3930 self.hide_show_widgets()
3932 def download_status_progress(self, progress, speed):
3933 # We receive this from the main window every time the progress
3934 # for our episode has changed (but only when this window is visible)
3935 self.download_progress.set_fraction(progress)
3936 self.download_progress.set_text('Downloading: %d%% (%s/s)' % (100.*progress, util.format_filesize(speed)))
3938 def hide_show_widgets(self):
3939 is_downloading = self.episode_is_downloading(self.episode)
3940 if is_downloading:
3941 self.download_progress.show_all()
3942 self.btnCancel.show_all()
3943 self.btnPlay.hide_all()
3944 self.btnDownload.hide_all()
3945 else:
3946 self.download_progress.hide_all()
3947 self.btnCancel.hide_all()
3948 if self.episode.was_downloaded(and_exists=True):
3949 if self.episode.file_type() in ('audio', 'video'):
3950 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3951 else:
3952 self.btnPlay.set_label(gtk.STOCK_OPEN)
3953 self.btnPlay.set_use_stock(True)
3954 self.btnPlay.show_all()
3955 self.btnDownload.hide_all()
3956 else:
3957 self.btnPlay.hide_all()
3958 self.btnDownload.show_all()
3960 def on_download(self, widget):
3961 if self.download_callback:
3962 self.download_callback()
3964 def on_playback(self, widget):
3965 if self.play_callback:
3966 self.play_callback()
3967 self.on_close(widget)
3969 class gPodderSync(BuilderWidget):
3970 def new(self):
3971 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3973 self.device.register('progress', self.on_progress)
3974 self.device.register('sub-progress', self.on_sub_progress)
3975 self.device.register('status', self.on_status)
3976 self.device.register('done', self.on_done)
3978 def on_progress(self, pos, max, text=None):
3979 if text is None:
3980 text = _('%d of %d done') % (pos, max)
3981 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3982 util.idle_add(self.progressbar.set_text, text)
3984 def on_sub_progress(self, percentage):
3985 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3987 def on_status(self, status):
3988 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3990 def on_done(self):
3991 util.idle_add(self.gPodderSync.destroy)
3993 def on_gPodderSync_destroy(self, widget, *args):
3994 self.device.unregister('progress', self.on_progress)
3995 self.device.unregister('sub-progress', self.on_sub_progress)
3996 self.device.unregister('status', self.on_status)
3997 self.device.unregister('done', self.on_done)
3998 self.device.cancel()
4000 def on_cancel_button_clicked(self, widget, *args):
4001 self.device.cancel()
4004 class gPodderOpmlLister(BuilderWidget):
4005 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
4006 (MODE_DOWNLOAD, MODE_SEARCH) = range(2)
4008 def new(self):
4009 # initiate channels list
4010 self.channels = []
4011 self.callback_for_channel = None
4012 self.callback_finished = None
4014 if hasattr(self, 'custom_title'):
4015 self.gPodderOpmlLister.set_title(self.custom_title)
4016 if hasattr(self, 'hide_url_entry'):
4017 self.hboxOpmlUrlEntry.hide_all()
4018 new_parent = self.notebookChannelAdder.get_parent()
4019 new_parent.remove(self.notebookChannelAdder)
4020 self.vboxOpmlImport.reparent(new_parent)
4022 self.setup_treeview(self.treeviewChannelChooser)
4023 self.setup_treeview(self.treeviewTopPodcastsChooser)
4024 self.setup_treeview(self.treeviewYouTubeChooser)
4026 self.current_mode = self.MODE_DOWNLOAD
4028 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
4030 def setup_treeview(self, tv):
4031 togglecell = gtk.CellRendererToggle()
4032 togglecell.set_property( 'activatable', True)
4033 togglecell.connect( 'toggled', self.callback_edited)
4034 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=OpmlListModel.C_SELECTED)
4036 titlecell = gtk.CellRendererText()
4037 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
4038 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=OpmlListModel.C_DESCRIPTION_MARKUP)
4040 for itemcolumn in (togglecolumn, titlecolumn):
4041 tv.append_column(itemcolumn)
4043 def callback_edited( self, cell, path):
4044 model = self.get_treeview().get_model()
4046 url = model[path][OpmlListModel.C_URL]
4048 model[path][OpmlListModel.C_SELECTED] = not model[path][OpmlListModel.C_SELECTED]
4049 if model[path][OpmlListModel.C_SELECTED]:
4050 self.channels.append( url)
4051 else:
4052 self.channels.remove( url)
4054 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
4056 def on_entryURL_changed(self, editable):
4057 old_mode = self.current_mode
4058 self.current_mode = not editable.get_text().lower().startswith('http://')
4059 if self.current_mode == old_mode:
4060 return
4062 if self.current_mode == self.MODE_SEARCH:
4063 self.btnDownloadOpml.set_property('image', None)
4064 self.btnDownloadOpml.set_label(gtk.STOCK_FIND)
4065 self.btnDownloadOpml.set_use_stock(True)
4066 self.labelOpmlUrl.set_text(_('Search podcast.de:'))
4067 else:
4068 self.btnDownloadOpml.set_label(_('Download'))
4069 self.btnDownloadOpml.set_image(gtk.image_new_from_stock(gtk.STOCK_GOTO_BOTTOM, gtk.ICON_SIZE_BUTTON))
4070 self.btnDownloadOpml.set_use_stock(False)
4071 self.labelOpmlUrl.set_text(_('OPML:'))
4073 def get_selected_channels(self, tab=None):
4074 channels = []
4076 model = self.get_treeview(tab).get_model()
4077 if model is not None:
4078 for row in model:
4079 if row[OpmlListModel.C_SELECTED]:
4080 channels.append(row[OpmlListModel.C_URL])
4082 return channels
4084 def on_change_tab(self, tab):
4085 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
4087 def thread_finished(self, model, tab=0):
4088 if tab == 1:
4089 tv = self.treeviewTopPodcastsChooser
4090 elif tab == 2:
4091 tv = self.treeviewYouTubeChooser
4092 self.entryYoutubeSearch.set_sensitive(True)
4093 self.btnSearchYouTube.set_sensitive(True)
4094 self.btnOK.set_sensitive(False)
4095 else:
4096 tv = self.treeviewChannelChooser
4097 self.btnDownloadOpml.set_sensitive(True)
4098 self.entryURL.set_sensitive(True)
4099 self.channels = []
4101 tv.set_model(model)
4102 tv.set_sensitive(True)
4104 def thread_func(self, tab=0):
4105 if tab == 1:
4106 model = OpmlListModel(opml.Importer(gl.config.toplist_url))
4107 if len(model) == 0:
4108 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4109 elif tab == 2:
4110 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
4111 if len(model) == 0:
4112 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
4113 else:
4114 url = self.entryURL.get_text()
4115 if not os.path.isfile(url) and not url.lower().startswith('http://'):
4116 log('Using podcast.de search')
4117 url = 'http://api.podcast.de/opml/podcasts/suche/%s' % (urllib.quote(url),)
4118 model = OpmlListModel(opml.Importer(url))
4119 if len(model) == 0:
4120 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4122 util.idle_add(self.thread_finished, model, tab)
4124 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
4125 if callback_for_channel:
4126 self.callback_for_channel = callback_for_channel
4127 if callback_finished:
4128 self.callback_finished = callback_finished
4129 self.entryURL.set_text( url)
4130 self.btnDownloadOpml.set_sensitive( False)
4131 self.entryURL.set_sensitive( False)
4132 self.btnOK.set_sensitive( False)
4133 self.treeviewChannelChooser.set_sensitive( False)
4134 Thread( target = self.thread_func).start()
4135 Thread( target = lambda: self.thread_func(1)).start()
4137 def select_all( self, value ):
4138 enabled = False
4139 model = self.get_treeview().get_model()
4140 if model is not None:
4141 for row in model:
4142 row[OpmlListModel.C_SELECTED] = value
4143 if value:
4144 enabled = True
4145 self.btnOK.set_sensitive(enabled)
4147 def on_gPodderOpmlLister_destroy(self, widget, *args):
4148 pass
4150 def on_btnDownloadOpml_clicked(self, widget, *args):
4151 self.get_channels_from_url( self.entryURL.get_text())
4153 def on_btnSearchYouTube_clicked(self, widget, *args):
4154 self.entryYoutubeSearch.set_sensitive(False)
4155 self.treeviewYouTubeChooser.set_sensitive(False)
4156 self.btnSearchYouTube.set_sensitive(False)
4157 Thread(target = lambda: self.thread_func(2)).start()
4159 def on_btnSelectAll_clicked(self, widget, *args):
4160 self.select_all(True)
4162 def on_btnSelectNone_clicked(self, widget, *args):
4163 self.select_all(False)
4165 def on_btnOK_clicked(self, widget, *args):
4166 self.channels = self.get_selected_channels()
4167 self.gPodderOpmlLister.destroy()
4169 # add channels that have been selected
4170 for url in self.channels:
4171 if self.callback_for_channel:
4172 self.callback_for_channel( url)
4174 if self.callback_finished:
4175 util.idle_add(self.callback_finished)
4177 def on_btnCancel_clicked(self, widget, *args):
4178 self.gPodderOpmlLister.destroy()
4180 def on_entryYoutubeSearch_key_press_event(self, widget, event):
4181 if event.keyval == gtk.keysyms.Return:
4182 self.on_btnSearchYouTube_clicked(widget)
4184 def get_treeview(self, tab=None):
4185 if tab is None:
4186 tab = self.notebookChannelAdder.get_current_page()
4188 if tab == 0:
4189 return self.treeviewChannelChooser
4190 elif tab == 1:
4191 return self.treeviewTopPodcastsChooser
4192 else:
4193 return self.treeviewYouTubeChooser
4195 class gPodderEpisodeSelector( BuilderWidget):
4196 """Episode selection dialog
4198 Optional keyword arguments that modify the behaviour of this dialog:
4200 - callback: Function that takes 1 parameter which is a list of
4201 the selected episodes (or empty list when none selected)
4202 - remove_callback: Function that takes 1 parameter which is a list
4203 of episodes that should be "removed" (see below)
4204 (default is None, which means remove not possible)
4205 - remove_action: Label for the "remove" action (default is "Remove")
4206 - remove_finished: Callback after all remove callbacks have finished
4207 (default is None, also depends on remove_callback)
4208 It will get a list of episode URLs that have been
4209 removed, so the main UI can update those
4210 - episodes: List of episodes that are presented for selection
4211 - selected: (optional) List of boolean variables that define the
4212 default checked state for the given episodes
4213 - selected_default: (optional) The default boolean value for the
4214 checked state if no other value is set
4215 (default is False)
4216 - columns: List of (name, sort_name, sort_type, caption) pairs for the
4217 columns, the name is the attribute name of the episode to be
4218 read from each episode object. The sort name is the
4219 attribute name of the episode to be used to sort this column.
4220 If the sort_name is None it will use the attribute name for
4221 sorting. The sort type is the type of the sort column.
4222 The caption attribute is the text that appear as column caption
4223 (default is [('title_markup', None, None, 'Episode'),])
4224 - title: (optional) The title of the window + heading
4225 - instructions: (optional) A one-line text describing what the
4226 user should select / what the selection is for
4227 - stock_ok_button: (optional) Will replace the "OK" button with
4228 another GTK+ stock item to be used for the
4229 affirmative button of the dialog (e.g. can
4230 be gtk.STOCK_DELETE when the episodes to be
4231 selected will be deleted after closing the
4232 dialog)
4233 - selection_buttons: (optional) A dictionary with labels as
4234 keys and callbacks as values; for each
4235 key a button will be generated, and when
4236 the button is clicked, the callback will
4237 be called for each episode and the return
4238 value of the callback (True or False) will
4239 be the new selected state of the episode
4240 - size_attribute: (optional) The name of an attribute of the
4241 supplied episode objects that can be used to
4242 calculate the size of an episode; set this to
4243 None if no total size calculation should be
4244 done (in cases where total size is useless)
4245 (default is 'length')
4246 - tooltip_attribute: (optional) The name of an attribute of
4247 the supplied episode objects that holds
4248 the text for the tooltips when hovering
4249 over an episode (default is 'description')
4252 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
4254 COLUMN_INDEX = 0
4255 COLUMN_TOOLTIP = 1
4256 COLUMN_TOGGLE = 2
4257 COLUMN_ADDITIONAL = 3
4259 def new( self):
4260 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
4261 if not hasattr( self, 'callback'):
4262 self.callback = None
4264 if not hasattr(self, 'remove_callback'):
4265 self.remove_callback = None
4267 if not hasattr(self, 'remove_action'):
4268 self.remove_action = _('Remove')
4270 if not hasattr(self, 'remove_finished'):
4271 self.remove_finished = None
4273 if not hasattr( self, 'episodes'):
4274 self.episodes = []
4276 if not hasattr( self, 'size_attribute'):
4277 self.size_attribute = 'length'
4279 if not hasattr(self, 'tooltip_attribute'):
4280 self.tooltip_attribute = 'description'
4282 if not hasattr( self, 'selection_buttons'):
4283 self.selection_buttons = {}
4285 if not hasattr( self, 'selected_default'):
4286 self.selected_default = False
4288 if not hasattr( self, 'selected'):
4289 self.selected = [self.selected_default]*len(self.episodes)
4291 if len(self.selected) < len(self.episodes):
4292 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
4294 if not hasattr( self, 'columns'):
4295 self.columns = (('title_markup', None, None, _('Episode')),)
4297 if hasattr( self, 'title'):
4298 self.gPodderEpisodeSelector.set_title( self.title)
4299 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
4301 if gpodder.interface == gpodder.MAEMO:
4302 self.labelHeading.hide()
4304 if hasattr( self, 'instructions'):
4305 self.labelInstructions.set_text( self.instructions)
4306 self.labelInstructions.show_all()
4308 if hasattr(self, 'stock_ok_button'):
4309 if self.stock_ok_button == 'gpodder-download':
4310 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
4311 self.btnOK.set_label(_('Download'))
4312 else:
4313 self.btnOK.set_label(self.stock_ok_button)
4314 self.btnOK.set_use_stock(True)
4316 # check/uncheck column
4317 toggle_cell = gtk.CellRendererToggle()
4318 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
4319 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
4321 next_column = self.COLUMN_ADDITIONAL
4322 for name, sort_name, sort_type, caption in self.columns:
4323 renderer = gtk.CellRendererText()
4324 if next_column < self.COLUMN_ADDITIONAL + 2:
4325 renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
4326 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
4327 column.set_resizable( True)
4328 # Only set "expand" on the first two columns
4329 if next_column < self.COLUMN_ADDITIONAL + 2:
4330 column.set_expand(True)
4331 if sort_name is not None:
4332 column.set_sort_column_id(next_column+1)
4333 else:
4334 column.set_sort_column_id(next_column)
4335 self.treeviewEpisodes.append_column( column)
4336 next_column += 1
4338 if sort_name is not None:
4339 # add the sort column
4340 column = gtk.TreeViewColumn()
4341 column.set_visible(False)
4342 self.treeviewEpisodes.append_column( column)
4343 next_column += 1
4345 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
4346 # add string column type plus sort column type if it exists
4347 for name, sort_name, sort_type, caption in self.columns:
4348 column_types.append(gobject.TYPE_STRING)
4349 if sort_name is not None:
4350 column_types.append(sort_type)
4351 self.model = gtk.ListStore( *column_types)
4353 tooltip = None
4354 for index, episode in enumerate( self.episodes):
4355 if self.tooltip_attribute is not None:
4356 try:
4357 tooltip = getattr(episode, self.tooltip_attribute)
4358 except:
4359 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
4360 tooltip = None
4361 row = [ index, tooltip, self.selected[index] ]
4362 for name, sort_name, sort_type, caption in self.columns:
4363 if not hasattr(episode, name):
4364 log('Warning: Missing attribute "%s"', name, sender=self)
4365 row.append(None)
4366 else:
4367 row.append(getattr( episode, name))
4369 if sort_name is not None:
4370 if not hasattr(episode, sort_name):
4371 log('Warning: Missing attribute "%s"', sort_name, sender=self)
4372 row.append(None)
4373 else:
4374 row.append(getattr( episode, sort_name))
4375 self.model.append( row)
4377 if self.remove_callback is not None:
4378 self.btnRemoveAction.show()
4379 self.btnRemoveAction.set_label(self.remove_action)
4381 # connect to tooltip signals
4382 if self.tooltip_attribute is not None:
4383 try:
4384 self.treeviewEpisodes.set_property('has-tooltip', True)
4385 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
4386 except:
4387 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
4388 self.last_tooltip_episode = None
4389 self.episode_list_can_tooltip = True
4391 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
4392 self.treeviewEpisodes.set_rules_hint( True)
4393 self.treeviewEpisodes.set_model( self.model)
4394 self.treeviewEpisodes.columns_autosize()
4395 self.calculate_total_size()
4397 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
4398 # With get_bin_window, we get the window that contains the rows without
4399 # the header. The Y coordinate of this window will be the height of the
4400 # treeview header. This is the amount we have to subtract from the
4401 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
4402 (x_bin, y_bin) = treeview.get_bin_window().get_position()
4403 y -= x_bin
4404 y -= y_bin
4405 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
4407 if not self.episode_list_can_tooltip:
4408 self.last_tooltip_episode = None
4409 return False
4411 if path is not None:
4412 model = treeview.get_model()
4413 iter = model.get_iter(path)
4414 index = model.get_value(iter, self.COLUMN_INDEX)
4415 description = model.get_value(iter, self.COLUMN_TOOLTIP)
4416 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
4417 self.last_tooltip_episode = None
4418 return False
4419 self.last_tooltip_episode = index
4421 if description is not None:
4422 tooltip.set_text(description)
4423 return True
4424 else:
4425 return False
4427 self.last_tooltip_episode = None
4428 return False
4430 def treeview_episodes_button_pressed(self, treeview, event):
4431 if event.button == 3:
4432 menu = gtk.Menu()
4434 if len(self.selection_buttons):
4435 for label in self.selection_buttons:
4436 item = gtk.MenuItem(label)
4437 item.connect('activate', self.custom_selection_button_clicked, label)
4438 menu.append(item)
4439 menu.append(gtk.SeparatorMenuItem())
4441 item = gtk.MenuItem(_('Select all'))
4442 item.connect('activate', self.on_btnCheckAll_clicked)
4443 menu.append(item)
4445 item = gtk.MenuItem(_('Select none'))
4446 item.connect('activate', self.on_btnCheckNone_clicked)
4447 menu.append(item)
4449 menu.show_all()
4450 # Disable tooltips while we are showing the menu, so
4451 # the tooltip will not appear over the menu
4452 self.episode_list_can_tooltip = False
4453 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
4454 menu.popup(None, None, None, event.button, event.time)
4456 return True
4458 def episode_list_allow_tooltips(self):
4459 self.episode_list_can_tooltip = True
4461 def calculate_total_size( self):
4462 if self.size_attribute is not None:
4463 (total_size, count) = (0, 0)
4464 for episode in self.get_selected_episodes():
4465 try:
4466 total_size += int(getattr( episode, self.size_attribute))
4467 count += 1
4468 except:
4469 log( 'Cannot get size for %s', episode.title, sender = self)
4471 text = []
4472 if count == 0:
4473 text.append(_('Nothing selected'))
4474 elif count == 1:
4475 text.append(_('One episode selected'))
4476 else:
4477 text.append(_('%d episodes selected') % count)
4478 if total_size > 0:
4479 text.append(_('total size: %s') % util.format_filesize(total_size))
4480 self.labelTotalSize.set_text(', '.join(text))
4481 self.btnOK.set_sensitive(count>0)
4482 self.btnRemoveAction.set_sensitive(count>0)
4483 if count > 0:
4484 self.btnCancel.set_label(gtk.STOCK_CANCEL)
4485 else:
4486 self.btnCancel.set_label(gtk.STOCK_CLOSE)
4487 else:
4488 self.btnOK.set_sensitive(False)
4489 self.btnRemoveAction.set_sensitive(False)
4490 for index, row in enumerate(self.model):
4491 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
4492 self.btnOK.set_sensitive(True)
4493 self.btnRemoveAction.set_sensitive(True)
4494 break
4495 self.labelTotalSize.set_text('')
4497 def toggle_cell_handler( self, cell, path):
4498 model = self.treeviewEpisodes.get_model()
4499 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
4501 self.calculate_total_size()
4503 def custom_selection_button_clicked(self, button, label):
4504 callback = self.selection_buttons[label]
4506 for index, row in enumerate( self.model):
4507 new_value = callback( self.episodes[index])
4508 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
4510 self.calculate_total_size()
4512 def on_btnCheckAll_clicked( self, widget):
4513 for row in self.model:
4514 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
4516 self.calculate_total_size()
4518 def on_btnCheckNone_clicked( self, widget):
4519 for row in self.model:
4520 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
4522 self.calculate_total_size()
4524 def on_remove_action_activate(self, widget):
4525 episodes = self.get_selected_episodes(remove_episodes=True)
4527 urls = []
4528 for episode in episodes:
4529 urls.append(episode.url)
4530 self.remove_callback(episode)
4532 if self.remove_finished is not None:
4533 self.remove_finished(urls)
4534 self.calculate_total_size()
4536 def get_selected_episodes( self, remove_episodes=False):
4537 selected_episodes = []
4539 for index, row in enumerate( self.model):
4540 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
4541 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
4543 if remove_episodes:
4544 for episode in selected_episodes:
4545 index = self.episodes.index(episode)
4546 iter = self.model.get_iter_first()
4547 while iter is not None:
4548 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
4549 self.model.remove(iter)
4550 break
4551 iter = self.model.iter_next(iter)
4553 return selected_episodes
4555 def on_btnOK_clicked( self, widget):
4556 self.gPodderEpisodeSelector.destroy()
4557 if self.callback is not None:
4558 self.callback( self.get_selected_episodes())
4560 def on_btnCancel_clicked( self, widget):
4561 self.gPodderEpisodeSelector.destroy()
4562 if self.callback is not None:
4563 self.callback([])
4565 class gPodderConfigEditor(BuilderWidget):
4566 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
4568 def new(self):
4569 name_column = gtk.TreeViewColumn(_('Setting'))
4570 name_renderer = gtk.CellRendererText()
4571 name_column.pack_start(name_renderer)
4572 name_column.add_attribute(name_renderer, 'text', 0)
4573 name_column.add_attribute(name_renderer, 'style', 5)
4574 self.configeditor.append_column(name_column)
4576 value_column = gtk.TreeViewColumn(_('Set to'))
4577 value_check_renderer = gtk.CellRendererToggle()
4578 value_column.pack_start(value_check_renderer, expand=False)
4579 value_column.add_attribute(value_check_renderer, 'active', 7)
4580 value_column.add_attribute(value_check_renderer, 'visible', 6)
4581 value_column.add_attribute(value_check_renderer, 'activatable', 6)
4582 value_check_renderer.connect('toggled', self.value_toggled)
4584 value_renderer = gtk.CellRendererText()
4585 value_column.pack_start(value_renderer)
4586 value_column.add_attribute(value_renderer, 'text', 2)
4587 value_column.add_attribute(value_renderer, 'visible', 4)
4588 value_column.add_attribute(value_renderer, 'editable', 4)
4589 value_column.add_attribute(value_renderer, 'style', 5)
4590 value_renderer.connect('edited', self.value_edited)
4591 self.configeditor.append_column(value_column)
4593 self.model = gl.config.model()
4594 self.filter = self.model.filter_new()
4595 self.filter.set_visible_func(self.visible_func)
4597 self.configeditor.set_model(self.filter)
4598 self.configeditor.set_rules_hint(True)
4599 self.configeditor.get_selection().connect( 'changed',
4600 self.on_configeditor_row_changed )
4602 def visible_func(self, model, iter, user_data=None):
4603 text = self.entryFilter.get_text().lower()
4604 if text == '':
4605 return True
4606 else:
4607 # either the variable name or its value
4608 return (text in model.get_value(iter, 0).lower() or
4609 text in model.get_value(iter, 2).lower())
4611 def value_edited(self, renderer, path, new_text):
4612 model = self.configeditor.get_model()
4613 iter = model.get_iter(path)
4614 name = model.get_value(iter, 0)
4615 type_cute = model.get_value(iter, 1)
4617 if not gl.config.update_field(name, new_text):
4618 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))
4620 def value_toggled(self, renderer, path):
4621 model = self.configeditor.get_model()
4622 iter = model.get_iter(path)
4623 field_name = model.get_value(iter, 0)
4624 field_type = model.get_value(iter, 3)
4626 # Flip the boolean config flag
4627 if field_type == bool:
4628 gl.config.toggle_flag(field_name)
4630 def on_entryFilter_changed(self, widget):
4631 self.filter.refilter()
4633 def on_btnShowAll_clicked(self, widget):
4634 self.entryFilter.set_text('')
4635 self.entryFilter.grab_focus()
4637 def on_btnClose_clicked(self, widget):
4638 self.gPodderConfigEditor.destroy()
4640 def on_configeditor_row_changed(self, treeselection):
4641 model, iter = treeselection.get_selected()
4642 if iter is not None:
4643 option_name = gl.config.get_description( model.get(iter, 0)[0] )
4644 self.config_option_description_label.set_text(option_name)
4646 class gPodderPlaylist(BuilderWidget):
4647 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4649 def new(self):
4650 self.linebreak = '\n'
4651 if gl.config.mp3_player_playlist_win_path:
4652 self.linebreak = '\r\n'
4653 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
4654 if self.mountpoint == '/':
4655 self.mountpoint = gl.config.mp3_player_folder
4656 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
4657 self.playlist_file = os.path.join(self.mountpoint,
4658 gl.config.mp3_player_playlist_file)
4659 icon_theme = gtk.icon_theme_get_default()
4660 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4662 # add column two
4663 check_cell = gtk.CellRendererToggle()
4664 check_cell.set_property('activatable', True)
4665 check_cell.connect('toggled', self.cell_toggled)
4666 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4667 self.treeviewPlaylist.append_column(check_column)
4669 # add column three
4670 column = gtk.TreeViewColumn(_('Filename'))
4671 icon_cell = gtk.CellRendererPixbuf()
4672 column.pack_start(icon_cell, False)
4673 column.add_attribute(icon_cell, 'pixbuf', 0)
4674 filename_cell = gtk.CellRendererText()
4675 column.pack_start(filename_cell, True)
4676 column.add_attribute(filename_cell, 'text', 2)
4678 column.set_resizable(True)
4679 self.treeviewPlaylist.append_column(column)
4681 # Make treeview reorderable
4682 self.treeviewPlaylist.set_reorderable(True)
4684 # init liststore
4685 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4686 self.treeviewPlaylist.set_model(self.playlist)
4688 # read device and playlist and fill the TreeView
4689 title = _('Reading files from %s') % gl.config.mp3_player_folder
4690 message = _('Please wait your media file list is being read from device.')
4691 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
4692 dlg.set_title(title)
4693 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
4694 dlg.show_all()
4695 Thread(target=self.process_device, args=[dlg]).start()
4697 def process_device(self, dlg):
4698 self.m3u = self.read_m3u()
4699 self.device = self.read_device()
4700 util.idle_add(self.write2gui, dlg)
4702 def cell_toggled(self, cellrenderertoggle, path):
4703 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4704 it = liststore.get_iter(path)
4705 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4707 def on_btnCancelPlaylist_clicked(self, widget):
4708 self.gPodderPlaylist.destroy()
4710 def on_btnSavePlaylist_clicked(self, widget):
4711 self.write_m3u()
4712 self.gPodderPlaylist.destroy()
4714 def read_m3u(self):
4716 read all files from the existing playlist
4718 tracks = []
4719 log("Read data from the playlistfile %s" % self.playlist_file)
4720 if os.path.exists(self.playlist_file):
4721 for line in open(self.playlist_file, 'r'):
4722 if not line.startswith('#EXT'):
4723 if line.startswith('#'):
4724 tracks.append([False, line[1:].strip()])
4725 else:
4726 tracks.append([True, line.strip()])
4727 return tracks
4729 def build_extinf(self, filename):
4730 if gl.config.mp3_player_playlist_win_path:
4731 filename = filename.replace('\\', os.sep)
4733 # rebuild the whole filename including the mountpoint
4734 if gl.config.mp3_player_playlist_absolute_path:
4735 absfile = self.mountpoint + filename
4736 else:
4737 absfile = util.rel2abs(filename, os.path.dirname(self.playlist_file))
4739 # fallback: use the basename of the file
4740 (title, extension) = os.path.splitext(os.path.basename(filename))
4742 return "#EXTINF:0,%s%s" % (title.strip(), self.linebreak)
4744 def write_m3u(self):
4746 write the list into the playlist on the device
4748 log('Writing playlist file: %s', self.playlist_file, sender=self)
4749 playlist_folder = os.path.split(self.playlist_file)[0]
4750 if not util.make_directory(playlist_folder):
4751 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4752 else:
4753 try:
4754 fp = open(self.playlist_file, 'w')
4755 fp.write('#EXTM3U%s' % self.linebreak)
4756 for icon, checked, filename in self.playlist:
4757 fp.write(self.build_extinf(filename))
4758 if not checked:
4759 fp.write('#')
4760 fp.write(filename)
4761 fp.write(self.linebreak)
4762 fp.close()
4763 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4764 except IOError, ioe:
4765 self.show_message(str(ioe), _('Error writing playlist file'))
4767 def read_device(self):
4769 read all files from the device
4771 log('Reading files from %s', gl.config.mp3_player_folder, sender=self)
4772 tracks = []
4773 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4774 for file in files:
4775 filename = os.path.join(root, file)
4777 if filename == self.playlist_file or fnmatch.fnmatch(filename, '*.dat') or fnmatch.fnmatch(filename, '*.DAT'):
4778 # We don't want to have our playlist file as
4779 # an entry in our file list, so skip it!
4780 # We also don't want to include dat files
4781 continue
4783 if gl.config.mp3_player_playlist_absolute_path:
4784 filename = filename[len(self.mountpoint):]
4785 else:
4786 filename = util.relpath(os.path.dirname(self.playlist_file),
4787 os.path.dirname(filename)) + \
4788 os.sep + os.path.basename(filename)
4790 if gl.config.mp3_player_playlist_win_path:
4791 filename = filename.replace(os.sep, '\\')
4793 tracks.append(filename)
4794 return tracks
4796 def write2gui(self, dlg):
4797 # add the files from the device to the list only when
4798 # they are not yet in the playlist
4799 # mark this files as NEW
4800 for filename in self.device[:]:
4801 m3ulist = [file[1] for file in self.m3u]
4802 if filename not in m3ulist:
4803 self.playlist.append([self.icon_new, False, filename])
4805 # add the files from the playlist to the list only when
4806 # they are on the device
4807 for checked, filename in self.m3u[:]:
4808 if filename in self.device:
4809 self.playlist.append([None, checked, filename])
4811 dlg.destroy()
4812 return False
4814 class gPodderDependencyManager(BuilderWidget):
4815 def new(self):
4816 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4817 self.treeview_components.append_column(col_name)
4818 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4819 self.treeview_components.append_column(col_installed)
4820 self.treeview_components.set_model(services.dependency_manager.get_model())
4821 self.btn_about.set_sensitive(False)
4822 self.btn_install.set_sensitive(False)
4824 def on_btn_about_clicked(self, widget):
4825 selection = self.treeview_components.get_selection()
4826 model, iter = selection.get_selected()
4827 if iter is not None:
4828 title = model.get_value(iter, 0)
4829 description = model.get_value(iter, 1)
4830 available = model.get_value(iter, 3)
4831 missing = model.get_value(iter, 4)
4833 if not available:
4834 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4836 self.show_message(description, title)
4838 def on_btn_install_clicked(self, widget):
4839 # TODO: Implement package manager integration
4840 pass
4842 def on_treeview_components_cursor_changed(self, treeview):
4843 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4844 # TODO: If installing is possible, enable btn_install
4846 def on_gPodderDependencyManager_response(self, dialog, response_id):
4847 self.gPodderDependencyManager.destroy()
4849 class gPodderWelcome(BuilderWidget):
4850 finger_friendly_widgets = ['btnOPML', 'btnMygPodder', 'btnCancel']
4852 def new(self):
4853 for widget in (self.btnOPML, self.btnMygPodder):
4854 for child in widget.get_children():
4855 if isinstance(child, gtk.Alignment):
4856 child.set_padding(20, 20, 20, 20)
4857 else:
4858 child.set_padding(20, 20)
4859 self.gPodderWelcome.show()
4861 def on_show_example_podcasts(self, button):
4862 self.gPodderWelcome.destroy()
4863 self.show_example_podcasts_callback(None)
4865 def on_setup_my_gpodder(self, gpodder):
4866 self.gPodderWelcome.destroy()
4867 self.setup_my_gpodder_callback(None)
4869 def on_btnCancel_clicked(self, button):
4870 self.gPodderWelcome.destroy()
4872 def main():
4873 gobject.threads_init()
4874 gtk.window_set_default_icon_name( 'gpodder')
4876 try:
4877 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
4878 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
4879 except dbus.exceptions.DBusException, dbe:
4880 log('Warning: Cannot get "on the bus".', traceback=True)
4881 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4882 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4883 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4884 dlg.set_title('gPodder')
4885 dlg.run()
4886 dlg.destroy()
4887 sys.exit(0)
4889 if gpodder.interface == gpodder.MAEMO and \
4890 not gl.config.disable_fingerscroll:
4891 GtkBuilderWidget.use_fingerscroll = True
4893 gp = gPodder(bus_name)
4894 gp.run()