Add sys.exit(0) again to the gPodder closing code
[gpodder.git] / src / gpodder / gui.py
blobfd1a324d8d959ffa63969f7c8c4845b70752e927
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
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
43 if gpodder.win32:
44 # Mock the required D-Bus interfaces with no-ops
45 class dbus:
46 class SessionBus:
47 def __init__(self, *args, **kwargs):
48 pass
49 class glib:
50 class DBusGMainLoop:
51 pass
52 class service:
53 @staticmethod
54 def method(interface):
55 return lambda x: x
56 class BusName:
57 def __init__(self, *args, **kwargs):
58 pass
59 class Object:
60 def __init__(self, *args, **kwargs):
61 pass
62 else:
63 import dbus
64 import dbus.service
65 import dbus.mainloop
66 import dbus.glib
69 from gpodder import libtagupdate
70 from gpodder import util
71 from gpodder import opml
72 from gpodder import services
73 from gpodder import sync
74 from gpodder import download
75 from gpodder import uibase
76 from gpodder import my
77 from gpodder import widgets
78 from gpodder.liblogger import log
79 from gpodder.dbsqlite import db
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 libpodcasts import PodcastChannel
93 from libpodcasts import LocalDBReader
94 from libpodcasts import channels_to_model
95 from libpodcasts import update_channel_model_by_iter
96 from libpodcasts import load_channels
97 from libpodcasts import update_channels
98 from libpodcasts import save_channels
99 from libpodcasts import can_restore_from_opml
100 from libpodcasts import HTTPAuthError
102 from gpodder.libgpodder import gl
104 from libplayers import UserAppsReader
106 from libtagupdate import tagging_supported
108 if gpodder.interface == gpodder.GUI:
109 WEB_BROWSER_ICON = 'web-browser'
110 elif gpodder.interface == gpodder.MAEMO:
111 import hildon
112 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
114 app_authors = [
115 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
117 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
118 'Alain Tauch', 'Alex Ghitza', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
119 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
120 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier', 'Casey Watson',
121 'Carlos Moffat', 'Chris Arnold', 'Chris Moffitt', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
122 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'Fabio Fiorentini', 'FFranci72', 'Florian Richter', 'Frank Harper',
123 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
124 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Iwan van der Kleijn', 'Jens Thiele',
125 'Jérôme Chabod', 'Jerry Moss',
126 'Jessica Henline', 'Jim Nygård', 'João Trindade', 'Joel Calado', 'John Ferguson',
127 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
128 'Jürgen Schinker', 'Justin Forest',
129 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marco Antonio Villegas Vega', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
130 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Morten Juhl-Johansen Zölde-Fejér', 'Mykola Nikishov', 'narf',
131 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
132 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
133 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
134 'Preben Randhol', 'Rafael Proença', 'R.Bell', 'red26wings', 'Richard Voigt',
135 'Robert Young', 'Roel Groeneveld', 'Romain Janvier',
136 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
137 'S. Rust',
138 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
139 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
140 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
141 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
143 'List may be incomplete - please contact me.'
146 class BuilderWidget(uibase.GtkBuilderWidget):
147 gpodder_main_window = None
148 finger_friendly_widgets = []
150 def __init__( self, **kwargs):
151 uibase.GtkBuilderWidget.__init__(self, gpodder.ui_folder, gpodder.textdomain, **kwargs)
153 # Set widgets to finger-friendly mode if on Maemo
154 for widget_name in self.finger_friendly_widgets:
155 if hasattr(self, widget_name):
156 self.set_finger_friendly(getattr(self, widget_name))
157 else:
158 log('Finger-friendly widget not found: %s', widget_name, sender=self)
160 if self.__class__.__name__ == 'gPodder':
161 BuilderWidget.gpodder_main_window = self.gPodder
162 else:
163 # If we have a child window, set it transient for our main window
164 self.main_window.set_transient_for(BuilderWidget.gpodder_main_window)
166 if gpodder.interface == gpodder.GUI:
167 if hasattr(self, 'center_on_widget'):
168 (x, y) = self.gpodder_main_window.get_position()
169 a = self.center_on_widget.allocation
170 (x, y) = (x + a.x, y + a.y)
171 (w, h) = (a.width, a.height)
172 (pw, ph) = self.main_window.get_size()
173 self.main_window.move(x + w/2 - pw/2, y + h/2 - ph/2)
174 else:
175 self.main_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
177 def notification(self, message, title=None):
178 util.idle_add(self.show_message, message, title)
180 def show_message( self, message, title = None):
181 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
182 if title is None:
183 title = 'gPodder'
184 self.tray_icon.send_notification(message, title)
185 return
187 if gpodder.interface == gpodder.GUI:
188 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
189 if title:
190 dlg.set_title(str(title))
191 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
192 else:
193 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
194 elif gpodder.interface == gpodder.MAEMO:
195 dlg = hildon.Note('information', (BuilderWidget.gpodder_main_window, message))
197 dlg.run()
198 dlg.destroy()
200 def set_finger_friendly(self, widget):
202 If we are on Maemo, we carry out the necessary
203 operations to turn a widget into a finger-friendly
204 one, depending on which type of widget it is (i.e.
205 buttons will have more padding, TreeViews a thick
206 scrollbar, etc..)
208 if gpodder.interface == gpodder.MAEMO:
209 if isinstance(widget, gtk.Misc):
210 widget.set_padding(0, 5)
211 elif isinstance(widget, gtk.Button):
212 for child in widget.get_children():
213 if isinstance(child, gtk.Alignment):
214 child.set_padding(5, 5, 5, 5)
215 else:
216 child.set_padding(5, 5)
217 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
218 parent = widget.get_parent()
219 if isinstance(parent, gtk.ScrolledWindow):
220 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
221 elif isinstance(widget, gtk.MenuItem):
222 for child in widget.get_children():
223 self.set_finger_friendly(child)
224 submenu = widget.get_submenu()
225 if submenu is not None:
226 for child in submenu.get_children():
227 self.set_finger_friendly(child)
228 elif isinstance(widget, gtk.Menu):
229 for child in widget.get_children():
230 self.set_finger_friendly(child)
231 else:
232 log('Cannot set widget finger-friendly: %s', widget, sender=self)
234 return widget
236 def show_confirmation( self, message, title = None):
237 if gpodder.interface == gpodder.GUI:
238 affirmative = gtk.RESPONSE_YES
239 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
240 if title:
241 dlg.set_title(str(title))
242 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
243 else:
244 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
245 elif gpodder.interface == gpodder.MAEMO:
246 affirmative = gtk.RESPONSE_OK
247 dlg = hildon.Note('confirmation', (BuilderWidget.gpodder_main_window, message))
249 response = dlg.run()
250 dlg.destroy()
252 return response == affirmative
254 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
255 """ An authentication dialog based on
256 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
258 dialog = gtk.MessageDialog(
259 BuilderWidget.gpodder_main_window,
260 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
261 gtk.MESSAGE_QUESTION,
262 gtk.BUTTONS_OK_CANCEL )
264 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
266 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
267 dialog.set_title(_('Authentication required'))
268 dialog.format_secondary_markup(message)
269 dialog.set_default_response(gtk.RESPONSE_OK)
271 if register_callback is not None:
272 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
274 username_entry = gtk.Entry()
275 password_entry = gtk.Entry()
277 username_entry.connect('activate', lambda w: password_entry.grab_focus())
278 password_entry.set_visibility(False)
279 password_entry.set_activates_default(True)
281 if username is not None:
282 username_entry.set_text(username)
283 if password is not None:
284 password_entry.set_text(password)
286 table = gtk.Table(2, 2)
287 table.set_row_spacings(6)
288 table.set_col_spacings(6)
290 username_label = gtk.Label()
291 username_label.set_markup('<b>' + username_prompt + ':</b>')
292 username_label.set_alignment(0.0, 0.5)
293 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
294 table.attach(username_entry, 1, 2, 0, 1)
296 password_label = gtk.Label()
297 password_label.set_markup('<b>' + _('Password') + ':</b>')
298 password_label.set_alignment(0.0, 0.5)
299 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
300 table.attach(password_entry, 1, 2, 1, 2)
302 dialog.vbox.pack_end(table, True, True, 0)
303 dialog.show_all()
304 response = dialog.run()
306 while response == gtk.RESPONSE_HELP:
307 register_callback()
308 response = dialog.run()
310 password_entry.set_visibility(True)
311 dialog.destroy()
313 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
315 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
316 if dst_filename is None:
317 dst_filename = src_filename
319 if dst_directory is None:
320 dst_directory = os.path.expanduser( '~')
322 ( base, extension ) = os.path.splitext( src_filename)
324 if not dst_filename.endswith( extension):
325 dst_filename += extension
327 if gpodder.interface == gpodder.GUI:
328 dlg = gtk.FileChooserDialog(title=title, parent=BuilderWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
329 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
330 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
331 elif gpodder.interface == gpodder.MAEMO:
332 dlg = hildon.FileChooserDialog(BuilderWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
334 dlg.set_do_overwrite_confirmation( True)
335 dlg.set_current_name( os.path.basename( dst_filename))
336 dlg.set_current_folder( dst_directory)
338 result = False
339 folder = dst_directory
340 if dlg.run() == gtk.RESPONSE_OK:
341 result = True
342 dst_filename = dlg.get_filename()
343 folder = dlg.get_current_folder()
344 if not dst_filename.endswith( extension):
345 dst_filename += extension
347 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
349 try:
350 shutil.copyfile( src_filename, dst_filename)
351 except:
352 log( 'Error copying file.', sender = self, traceback = True)
354 dlg.destroy()
355 return (result, folder)
358 class gPodder(BuilderWidget, dbus.service.Object):
359 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'itemQuit', 'menuPodcasts', 'menuSubscriptions', 'menuChannels', 'menuHelp']
360 ENTER_URL_TEXT = _('Enter podcast URL...')
362 def __init__(self, bus_name):
363 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
364 BuilderWidget.__init__(self)
366 def new(self):
367 if gpodder.interface == gpodder.MAEMO:
368 # Maemo-specific changes to the UI
369 gpodder.icon_file = gpodder.icon_file.replace('.svg', '.png')
371 self.app = hildon.Program()
372 gtk.set_application_name('gPodder')
373 self.window = hildon.Window()
374 self.window.connect('delete-event', self.on_gPodder_delete_event)
375 self.window.connect('window-state-event', self.window_state_event)
377 self.itemUpdateChannel.show()
378 self.UpdateChannelSeparator.show()
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.toolPreferences = gtk.ToolButton(gtk.STOCK_PREFERENCES)
418 self.toolPreferences.connect('clicked', self.on_itemPreferences_activate)
419 toolbar.insert(self.toolPreferences, -1)
420 self.toolPreferences.show()
422 self.toolQuit = gtk.ToolButton(gtk.STOCK_QUIT)
423 self.toolQuit.connect('clicked', self.on_gPodder_delete_event)
424 toolbar.insert(self.toolQuit, -1)
425 self.toolQuit.show()
427 # Add and replace toolbar with our new one
428 toolbar.show()
429 self.window.add_toolbar(toolbar)
430 self.toolbar = toolbar
432 self.app.add_window(self.window)
433 self.vMain.reparent(self.window)
434 self.gPodder = self.window
436 # Reparent the main menu
437 menu = gtk.Menu()
438 for child in self.mainMenu.get_children():
439 child.reparent(menu)
440 self.itemQuit.reparent(menu)
441 self.window.set_menu(menu)
443 self.mainMenu.destroy()
444 self.window.show()
446 # do some widget hiding
447 self.itemTransferSelected.hide_all()
448 self.item_email_subscriptions.hide_all()
449 self.menuView.hide()
451 # get screen real estate
452 self.hboxContainer.set_border_width(0)
454 # Offer importing of videocenter podcasts
455 if os.path.exists(os.path.expanduser('~/videocenter')):
456 self.item_upgrade_from_videocenter.show()
457 self.upgrade_from_videocenter_separator.show()
459 self.gPodder.connect('key-press-event', self.on_key_press)
460 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
462 if gpodder.win32:
463 # FIXME: Implement e-mail sending of list in win32
464 self.item_email_subscriptions.set_sensitive(False)
466 if gl.config.show_url_entry_in_podcast_list:
467 self.hboxAddChannel.show()
469 if not gpodder.interface == gpodder.MAEMO and not gl.config.show_toolbar:
470 self.toolbar.hide()
472 gl.config.add_observer(self.on_config_changed)
473 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
474 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
475 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
476 self.entry_add_channel_unfocus(self.entryAddChannel, None)
478 self.uar = None
479 self.tray_icon = None
480 self.gpodder_episode_window = None
482 self.download_status_manager = services.DownloadStatusManager()
483 self.download_queue_manager = download.DownloadQueueManager(self.download_status_manager)
485 self.fullscreen = False
486 self.minimized = False
487 self.gPodder.connect('window-state-event', self.window_state_event)
489 self.already_notified_new_episodes = []
490 self.show_hide_tray_icon()
492 self.itemShowToolbar.set_active(gl.config.show_toolbar)
493 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
495 gl.config.connect_gtk_window(self.gPodder, 'main_window')
496 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
498 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
499 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
500 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
501 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
503 # Then the amount of maximum downloads changes, notify the queue manager
504 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
505 self.spinMaxDownloads.connect('value-changed', changed_cb)
507 self.default_title = None
508 if gpodder.__version__.rfind('git') != -1:
509 self.set_title('gPodder %s' % gpodder.__version__)
510 else:
511 title = self.gPodder.get_title()
512 if title is not None:
513 self.set_title(title)
514 else:
515 self.set_title(_('gPodder'))
517 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
519 # cell renderers for channel tree
520 iconcolumn = gtk.TreeViewColumn('')
522 iconcell = gtk.CellRendererPixbuf()
523 iconcolumn.pack_start( iconcell, False)
524 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
525 self.cell_channel_icon = iconcell
527 namecolumn = gtk.TreeViewColumn('')
528 namecell = gtk.CellRendererText()
529 namecell.set_property('foreground-set', True)
530 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
531 namecolumn.pack_start( namecell, True)
532 namecolumn.add_attribute( namecell, 'markup', 2)
533 namecolumn.add_attribute( namecell, 'foreground', 8)
535 iconcell = gtk.CellRendererPixbuf()
536 iconcell.set_property('xalign', 1.0)
537 namecolumn.pack_start( iconcell, False)
538 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
539 namecolumn.add_attribute(iconcell, 'visible', 7)
540 self.cell_channel_pill = iconcell
542 self.treeChannels.set_enable_search(True)
543 self.treeChannels.set_search_column(1)
544 self.treeChannels.append_column(iconcolumn)
545 self.treeChannels.append_column(namecolumn)
546 self.treeChannels.set_headers_visible(False)
548 # enable alternating colors hint
549 self.treeAvailable.set_rules_hint( True)
550 self.treeChannels.set_rules_hint( True)
552 # connect to tooltip signals
553 try:
554 self.treeChannels.set_property('has-tooltip', True)
555 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
556 self.treeAvailable.set_property('has-tooltip', True)
557 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
558 except:
559 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
560 self.last_tooltip_channel = None
561 self.last_tooltip_episode = None
562 self.podcast_list_can_tooltip = True
563 self.episode_list_can_tooltip = True
565 self.currently_updating = False
567 # Add our context menu to treeAvailable
568 if gpodder.interface == gpodder.MAEMO:
569 self.treeview_available_buttonpress = (0, 0)
570 self.treeAvailable.connect('button-press-event', self.treeview_button_savepos)
571 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
573 self.treeview_channels_buttonpress = (0, 0)
574 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
575 self.treeChannels.connect('button-release-event', self.treeview_channels_button_released)
577 import mokoui
578 fs = mokoui.FingerScroll()
579 fs.set_property('spring-speed', 0)
580 self.treeAvailable.reparent(fs)
581 self.channelPaned.remove(self.scrollAvailable)
582 self.channelPaned.pack2(fs)
583 fs.show()
584 fsc = mokoui.FingerScroll()
585 fsc.set_property('spring-speed', 0)
586 self.treeChannels.reparent(fsc)
587 self.vboxChannelNavigator.remove(self.scrolledwindow6)
588 self.vboxChannelNavigator.pack_start(fsc, expand=True, fill=True)
589 self.vboxChannelNavigator.reorder_child(fsc, 0)
590 fsc.show()
591 else:
592 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
593 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
595 self.treeDownloads.connect('button-press-event', self.treeview_downloads_button_pressed)
597 iconcell = gtk.CellRendererPixbuf()
598 if gpodder.interface == gpodder.MAEMO:
599 iconcell.set_fixed_size(-1, 52)
600 status_column_label = ''
601 else:
602 status_column_label = _('Status')
603 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
605 namecell = gtk.CellRendererText()
606 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
607 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
608 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
609 namecolumn.set_resizable(True)
610 namecolumn.set_expand(True)
612 sizecell = gtk.CellRendererText()
613 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
615 releasecell = gtk.CellRendererText()
616 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
618 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
619 itemcolumn.set_reorderable(gpodder.interface != gpodder.MAEMO)
620 self.treeAvailable.append_column(itemcolumn)
622 if gpodder.interface == gpodder.MAEMO:
623 # Due to screen space contraints, we
624 # hide these columns here by default
625 self.column_size = sizecolumn
626 self.column_released = releasecolumn
627 self.column_released.set_visible(False)
628 self.column_size.set_visible(False)
630 # enable search in treeavailable
631 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
633 # enable multiple selection support
634 if gpodder.interface == gpodder.MAEMO:
635 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_SINGLE)
636 else:
637 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
638 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
640 if hasattr(self.treeDownloads, 'set_rubber_banding'):
641 # Available in PyGTK 2.10 and above
642 self.treeDownloads.set_rubber_banding(True)
644 # columns and renderers for "download progress" tab
645 DownloadStatusManager = services.DownloadStatusManager
647 # First column: [ICON] Episodename
648 column = gtk.TreeViewColumn(_('Episode'))
650 cell = gtk.CellRendererPixbuf()
651 if gpodder.interface == gpodder.MAEMO:
652 cell.set_property('stock-size', gtk.ICON_SIZE_DIALOG)
653 else:
654 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
655 column.pack_start(cell, expand=False)
656 column.add_attribute(cell, 'stock-id', \
657 DownloadStatusManager.C_ICON_NAME)
659 cell = gtk.CellRendererText()
660 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
661 column.pack_start(cell, expand=True)
662 column.add_attribute(cell, 'text', DownloadStatusManager.C_NAME)
664 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
665 column.set_resizable(True)
666 column.set_expand(True)
667 self.treeDownloads.append_column(column)
669 # Second column: Progress
670 column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
671 value=DownloadStatusManager.C_PROGRESS, \
672 text=DownloadStatusManager.C_PROGRESS_TEXT)
673 self.treeDownloads.append_column(column)
675 # Third column: Size
676 if gpodder.interface != gpodder.MAEMO:
677 column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
678 text=DownloadStatusManager.C_SIZE_TEXT)
679 self.treeDownloads.append_column(column)
681 # Fourth column: Speed
682 column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
683 text=DownloadStatusManager.C_SPEED_TEXT)
684 self.treeDownloads.append_column(column)
686 # Fifth column: Status
687 column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
688 text=DownloadStatusManager.C_STATUS_TEXT)
689 self.treeDownloads.append_column(column)
691 # After we've set up most of the window, show it :)
692 if not gpodder.interface == gpodder.MAEMO:
693 self.gPodder.show()
695 if gl.config.start_iconified:
696 self.iconify_main_window()
697 if self.tray_icon and gl.config.minimize_to_tray:
698 self.tray_icon.set_visible(False)
700 # a dictionary that maps episode URLs to the current
701 # treeAvailable row numbers to generate tree paths
702 self.url_path_mapping = {}
704 # a dictionary that maps channel URLs to the current
705 # treeChannels row numbers to generate tree paths
706 self.channel_url_path_mapping = {}
708 services.cover_downloader.register('cover-available', self.cover_download_finished)
709 services.cover_downloader.register('cover-removed', self.cover_file_removed)
710 self.cover_cache = {}
712 self.treeDownloads.set_model(self.download_status_manager.get_tree_model())
713 gobject.timeout_add(1500, self.update_downloads_list)
714 self.download_tasks_seen = set()
715 self.last_download_count = 0
717 #Add Drag and Drop Support
718 flags = gtk.DEST_DEFAULT_ALL
719 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
720 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
721 self.treeChannels.drag_dest_set( flags, targets, actions)
722 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
724 # Subscribed channels
725 self.active_channel = None
726 self.channels = load_channels()
727 self.channel_list_changed = True
728 self.update_podcasts_tab()
730 # load list of user applications for audio playback
731 self.user_apps_reader = UserAppsReader(['audio', 'video'])
732 Thread(target=self.read_apps).start()
734 # Set the "Device" menu item for the first time
735 self.update_item_device()
737 # Last folder used for saving episodes
738 self.folder_for_saving_episodes = None
740 # Set up default channel colors
741 self.channel_colors = {
742 'default': None,
743 'updating': gl.config.color_updating_feeds,
744 'parse_error': '#ff0000',
747 # Now, update the feed cache, when everything's in place
748 self.btnUpdateFeeds.show()
749 self.updated_feeds = 0
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 # Clean up old, orphaned download files
755 partial_files = gl.find_partial_files()
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 = os.path.basename(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.filename == 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
777 if len(resumable_episodes):
778 self.download_episode_list_paused(resumable_episodes)
779 self.message_area = widgets.SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
780 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
781 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
782 self.message_area.show_all()
783 self.wNotebook.set_current_page(1)
785 gl.clean_up_downloads(delete_partial=False)
786 else:
787 gl.clean_up_downloads(delete_partial=True)
789 # Start the auto-update procedure
790 self.auto_update_procedure(first_run=True)
792 # Delete old episodes if the user wishes to
793 if gl.config.auto_remove_old_episodes:
794 old_episodes = self.get_old_episodes()
795 if len(old_episodes) > 0:
796 self.delete_episode_list(old_episodes, confirm=False)
797 self.updateComboBox()
799 # First-time users should be asked if they want to see the OPML
800 if len(self.channels) == 0:
801 util.idle_add(self.on_itemUpdate_activate, None)
803 def update_downloads_list(self):
804 model = self.treeDownloads.get_model()
806 downloading, failed, finished, queued = 0, 0, 0, 0
807 total_speed, total_size, done_size = 0, 0, 0
809 # Keep a list of all download tasks that we've seen
810 download_tasks_seen = set()
812 # Remember the progress and speed for the episode that
813 # has been opened in the episode shownotes dialog (if any)
814 if self.gpodder_episode_window is not None:
815 episode_window_episode = self.gpodder_episode_window.episode
816 episode_window_progress = 0.0
817 episode_window_speed = 0.0
818 else:
819 episode_window_episode = None
821 for row in model:
822 self.download_status_manager.request_update(row.iter)
824 task = row[self.download_status_manager.C_TASK]
825 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
827 total_size += size
828 done_size += size*progress
830 if episode_window_episode is not None and \
831 episode_window_episode.url == task.url:
832 episode_window_progress = progress
833 episode_window_speed = speed
835 download_tasks_seen.add(task)
837 if status == download.DownloadTask.DOWNLOADING:
838 downloading += 1
839 total_speed += speed
840 elif status == download.DownloadTask.FAILED:
841 failed += 1
842 elif status == download.DownloadTask.DONE:
843 finished += 1
844 elif status == download.DownloadTask.QUEUED:
845 queued += 1
847 # Remember which tasks we have seen after this run
848 self.download_tasks_seen = download_tasks_seen
850 text = [_('Downloads')]
851 if downloading + failed + finished + queued > 0:
852 s = []
853 if downloading > 0:
854 s.append(_('%d downloading') % downloading)
855 if failed > 0:
856 s.append(_('%d failed') % failed)
857 if finished > 0:
858 s.append(_('%d done') % finished)
859 if queued > 0:
860 s.append(_('%d queued') % queued)
861 text.append(' (' + ', '.join(s)+')')
862 self.labelDownloads.set_text(''.join(text))
864 title = [self.default_title]
866 # We have to update all episode icons for which the status has changed
867 episode_urls = [task.url for task in self.download_tasks_seen if task.status_changed]
869 count = downloading + queued
870 if count > 0:
871 if count == 1:
872 title.append( _('downloading one file'))
873 elif count > 1:
874 title.append( _('downloading %d files') % count)
876 if total_size > 0:
877 percentage = 100.0*done_size/total_size
878 else:
879 percentage = 0.0
880 total_speed = gl.format_filesize(total_speed)
881 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
882 if self.tray_icon is not None:
883 # Update the tray icon status and progress bar
884 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
885 self.tray_icon.draw_progress_bar(percentage/100.)
886 elif self.last_download_count > 0:
887 if self.tray_icon is not None:
888 # Update the tray icon status
889 self.tray_icon.set_status()
890 self.tray_icon.downloads_finished(self.download_tasks_seen)
891 log('All downloads have finished.', sender=self)
892 if gl.config.cmd_all_downloads_complete:
893 util.run_external_command(gl.config.cmd_all_downloads_complete)
894 self.last_download_count = count
896 self.gPodder.set_title(' - '.join(title))
898 self.update_episode_list_icons(episode_urls)
899 if self.gpodder_episode_window is not None and \
900 self.gpodder_episode_window.gPodderEpisode.get_property('visible'):
901 self.gpodder_episode_window.download_status_changed(episode_urls)
902 self.gpodder_episode_window.download_status_progress(episode_window_progress, episode_window_speed)
903 self.play_or_download()
904 #self.updateComboBox(only_these_urls=channel_urls)
905 return True
907 def on_tree_channels_resize(self, widget, allocation):
908 if not gl.config.podcast_sidebar_save_space:
909 return
911 window_allocation = self.gPodder.get_allocation()
912 percentage = 100. * float(allocation.width) / float(window_allocation.width)
913 if hasattr(self, 'cell_channel_icon'):
914 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
915 if hasattr(self, 'cell_channel_pill'):
916 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
918 def entry_add_channel_focus(self, widget, event):
919 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
920 if widget.get_text() == self.ENTER_URL_TEXT:
921 widget.set_text('')
923 def entry_add_channel_unfocus(self, widget, event):
924 if widget.get_text() == '':
925 widget.set_text(self.ENTER_URL_TEXT)
926 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
928 def on_config_changed(self, name, old_value, new_value):
929 if name == 'show_toolbar' and gpodder.interface != gpodder.MAEMO:
930 if new_value:
931 self.toolbar.show()
932 else:
933 self.toolbar.hide()
934 elif name == 'episode_list_descriptions' and gpodder.interface != gpodder.MAEMO:
935 self.updateTreeView()
936 elif name == 'show_url_entry_in_podcast_list':
937 if new_value:
938 self.hboxAddChannel.show()
939 else:
940 self.hboxAddChannel.hide()
942 def read_apps(self):
943 time.sleep(3) # give other parts of gpodder a chance to start up
944 self.user_apps_reader.read()
945 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
946 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
948 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
949 # With get_bin_window, we get the window that contains the rows without
950 # the header. The Y coordinate of this window will be the height of the
951 # treeview header. This is the amount we have to subtract from the
952 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
953 (x_bin, y_bin) = treeview.get_bin_window().get_position()
954 y -= x_bin
955 y -= y_bin
956 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
958 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
959 self.last_tooltip_episode = None
960 return False
962 if path is not None:
963 model = treeview.get_model()
964 iter = model.get_iter(path)
965 url = model.get_value(iter, 0)
966 description = model.get_value(iter, 7) # FIXME INDEX MODEL BY SYMBOLIC NAME
967 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
968 self.last_tooltip_episode = None
969 return False
970 self.last_tooltip_episode = url
972 if len(description) > 400:
973 description = description[:398]+'[...]'
975 tooltip.set_text(description)
976 return True
978 self.last_tooltip_episode = None
979 return False
981 def podcast_list_allow_tooltips(self):
982 self.podcast_list_can_tooltip = True
984 def episode_list_allow_tooltips(self):
985 self.episode_list_can_tooltip = True
987 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
988 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
990 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
991 self.last_tooltip_channel = None
992 return False
994 if path is not None:
995 model = treeview.get_model()
996 iter = model.get_iter(path)
997 url = model.get_value(iter, 0)
998 for channel in self.channels:
999 if channel.url == url:
1000 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
1001 self.last_tooltip_channel = None
1002 return False
1003 self.last_tooltip_channel = channel
1004 channel.request_save_dir_size()
1005 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
1006 error_str = model.get_value(iter, 6)
1007 if error_str:
1008 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1009 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1010 table = gtk.Table(rows=3, columns=3)
1011 table.set_row_spacings(5)
1012 table.set_col_spacings(5)
1013 table.set_border_width(5)
1015 heading = gtk.Label()
1016 heading.set_alignment(0, 1)
1017 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1018 table.attach(heading, 0, 1, 0, 1)
1019 size_info = gtk.Label()
1020 size_info.set_alignment(1, 1)
1021 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1022 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1023 table.attach(size_info, 2, 3, 0, 1)
1025 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1027 if len(channel.description) < 500:
1028 description = channel.description
1029 else:
1030 pos = channel.description.find('\n\n')
1031 if pos == -1 or pos > 500:
1032 description = channel.description[:498]+'[...]'
1033 else:
1034 description = channel.description[:pos]
1036 description = gtk.Label(description)
1037 if error_str:
1038 description.set_markup(error_str)
1039 description.set_alignment(0, 0)
1040 description.set_line_wrap(True)
1041 table.attach(description, 0, 3, 2, 3)
1043 table.show_all()
1044 tooltip.set_custom(table)
1046 return True
1048 self.last_tooltip_channel = None
1049 return False
1051 def update_m3u_playlist_clicked(self, widget):
1052 self.active_channel.update_m3u_playlist()
1053 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
1055 def treeview_downloads_button_pressed(self, treeview, event):
1056 if event.button == 1:
1057 # Catch left mouse button presses, and if we there is no
1058 # path at the given position, deselect all items
1059 (x, y) = (int(event.x), int(event.y))
1060 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1061 if path is None:
1062 treeview.get_selection().unselect_all()
1064 # Use right-click for the Desktop version and left-click for Maemo
1065 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1066 (event.button == 3 and gpodder.interface == gpodder.GUI):
1067 (x, y) = (int(event.x), int(event.y))
1068 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1070 paths = []
1071 # Did the user right-click into a selection?
1072 selection = treeview.get_selection()
1073 if selection.count_selected_rows() and path:
1074 (model, paths) = selection.get_selected_rows()
1075 if path not in paths:
1076 # We have right-clicked, but not into the
1077 # selection, assume we don't want to operate
1078 # on the selection
1079 paths = []
1081 # No selection or right click not in selection:
1082 # Select the single item where we clicked
1083 if not paths and path:
1084 treeview.grab_focus()
1085 treeview.set_cursor( path, column, 0)
1086 (model, paths) = (treeview.get_model(), [path])
1088 # We did not find a selection, and the user didn't
1089 # click on an item to select -- don't show the menu
1090 if not paths:
1091 return True
1093 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
1095 def make_menu_item(label, stock_id, tasks, status):
1096 # This creates a menu item for selection-wide actions
1097 def for_each_task_set_status(tasks, status):
1098 changed_episode_urls = []
1099 for row_reference, task in tasks:
1100 if status is not None:
1101 if status == download.DownloadTask.QUEUED:
1102 # Only queue task when its paused/failed/cancelled
1103 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1104 self.download_queue_manager.add_task(task)
1105 elif status == download.DownloadTask.CANCELLED:
1106 # Cancelling a download only allows when paused/downloading/queued
1107 if task.status in (task.QUEUED, task.DOWNLOADING, task.PAUSED):
1108 task.status = status
1109 elif status == download.DownloadTask.PAUSED:
1110 # Pausing a download only when queued/downloading
1111 if task.status in (task.DOWNLOADING, task.QUEUED):
1112 task.status = status
1113 else:
1114 # We (hopefully) can simply set the task status here
1115 task.status = status
1116 else:
1117 # Remove the selected task - cancel downloading/queued tasks
1118 if task.status in (task.QUEUED, task.DOWNLOADING):
1119 task.status = task.CANCELLED
1120 model.remove(model.get_iter(row_reference.get_path()))
1121 # Remember the URL, so we can tell the UI to update
1122 try:
1123 # We don't "see" this task anymore - remove it;
1124 # this is needed, so update_episode_list_icons()
1125 # below gets the correct list of "seen" tasks
1126 self.download_tasks_seen.remove(task)
1127 except KeyError, key_error:
1128 log('Cannot remove task from "seen" list: %s', task, sender=self)
1129 changed_episode_urls.append(task.url)
1130 # Tell the task that it has been removed (so it can clean up)
1131 task.removed_from_list()
1132 # Tell the podcasts tab to update icons for our removed podcasts
1133 self.update_episode_list_icons(changed_episode_urls)
1134 return True
1135 item = gtk.ImageMenuItem(label)
1136 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1137 item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
1139 # Determine if we should disable this menu item
1140 for row_reference, task in tasks:
1141 if status == download.DownloadTask.QUEUED:
1142 if task.status not in (download.DownloadTask.PAUSED, \
1143 download.DownloadTask.FAILED, \
1144 download.DownloadTask.CANCELLED):
1145 item.set_sensitive(False)
1146 break
1147 elif status == download.DownloadTask.CANCELLED:
1148 if task.status not in (download.DownloadTask.PAUSED, \
1149 download.DownloadTask.QUEUED, \
1150 download.DownloadTask.DOWNLOADING):
1151 item.set_sensitive(False)
1152 break
1153 elif status == download.DownloadTask.PAUSED:
1154 if task.status not in (download.DownloadTask.QUEUED, \
1155 download.DownloadTask.DOWNLOADING):
1156 item.set_sensitive(False)
1157 break
1158 elif status is None:
1159 if task.status not in (download.DownloadTask.CANCELLED, \
1160 download.DownloadTask.FAILED, \
1161 download.DownloadTask.DONE):
1162 item.set_sensitive(False)
1163 break
1165 return self.set_finger_friendly(item)
1167 menu = gtk.Menu()
1169 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
1170 menu.append(gtk.SeparatorMenuItem())
1171 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
1172 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
1173 menu.append(gtk.SeparatorMenuItem())
1174 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
1176 if gpodder.interface == gpodder.MAEMO:
1177 # Because we open the popup on left-click for Maemo,
1178 # we also include a non-action to close the menu
1179 menu.append(gtk.SeparatorMenuItem())
1180 item = gtk.ImageMenuItem(_('Close this menu'))
1181 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1182 menu.append(self.set_finger_friendly(item))
1184 menu.show_all()
1185 menu.popup(None, None, None, event.button, event.time)
1186 return True
1189 def treeview_channels_button_pressed( self, treeview, event):
1190 global WEB_BROWSER_ICON
1192 if gpodder.interface == gpodder.MAEMO:
1193 self.treeview_channels_buttonpress = (event.x, event.y)
1194 return True
1196 if event.button == 3:
1197 ( x, y ) = ( int(event.x), int(event.y) )
1198 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1200 paths = []
1202 # Did the user right-click into a selection?
1203 selection = treeview.get_selection()
1204 if selection.count_selected_rows() and path:
1205 ( model, paths ) = selection.get_selected_rows()
1206 if path not in paths:
1207 # We have right-clicked, but not into the
1208 # selection, assume we don't want to operate
1209 # on the selection
1210 paths = []
1212 # No selection or right click not in selection:
1213 # Select the single item where we clicked
1214 if not len( paths) and path:
1215 treeview.grab_focus()
1216 treeview.set_cursor( path, column, 0)
1218 ( model, paths ) = ( treeview.get_model(), [ path ] )
1220 # We did not find a selection, and the user didn't
1221 # click on an item to select -- don't show the menu
1222 if not len( paths):
1223 return True
1225 menu = gtk.Menu()
1227 item = gtk.ImageMenuItem( _('Open download folder'))
1228 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
1229 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1230 menu.append( item)
1232 item = gtk.ImageMenuItem( _('Update Feed'))
1233 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
1234 item.connect('activate', self.on_itemUpdateChannel_activate )
1235 item.set_sensitive( not self.updating_feed_cache )
1236 menu.append( item)
1238 if gl.config.create_m3u_playlists:
1239 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1240 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1241 item.connect('activate', self.update_m3u_playlist_clicked)
1242 menu.append(item)
1244 if self.active_channel.link:
1245 item = gtk.ImageMenuItem(_('Visit website'))
1246 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1247 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1248 menu.append(item)
1250 if self.active_channel.channel_is_locked:
1251 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1252 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1253 item.connect('activate', self.on_channel_toggle_lock_activate)
1254 menu.append(self.set_finger_friendly(item))
1255 else:
1256 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1257 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1258 item.connect('activate', self.on_channel_toggle_lock_activate)
1259 menu.append(self.set_finger_friendly(item))
1262 menu.append( gtk.SeparatorMenuItem())
1264 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1265 item.connect( 'activate', self.on_itemEditChannel_activate)
1266 menu.append( item)
1268 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1269 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1270 menu.append( item)
1272 menu.show_all()
1273 # Disable tooltips while we are showing the menu, so
1274 # the tooltip will not appear over the menu
1275 self.podcast_list_can_tooltip = False
1276 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
1277 menu.popup( None, None, None, event.button, event.time)
1279 return True
1281 def on_itemClose_activate(self, widget):
1282 if self.tray_icon is not None:
1283 if gpodder.interface == gpodder.MAEMO:
1284 self.gPodder.set_property('visible', False)
1285 else:
1286 self.iconify_main_window()
1287 else:
1288 self.on_gPodder_delete_event(widget)
1290 def cover_file_removed(self, channel_url):
1292 The Cover Downloader calls this when a previously-
1293 available cover has been removed from the disk. We
1294 have to update our cache to reflect this change.
1296 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
1297 for row in self.treeChannels.get_model():
1298 if row[COLUMN_URL] == channel_url:
1299 row[COLUMN_PIXBUF] = None
1300 key = (channel_url, gl.config.podcast_list_icon_size, \
1301 gl.config.podcast_list_icon_size)
1302 if key in self.cover_cache:
1303 del self.cover_cache[key]
1306 def cover_download_finished(self, channel_url, pixbuf):
1308 The Cover Downloader calls this when it has finished
1309 downloading (or registering, if already downloaded)
1310 a new channel cover, which is ready for displaying.
1312 if pixbuf is not None:
1313 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
1314 model = self.treeChannels.get_model()
1315 if model is None:
1316 # Not yet ready (race condition) - simply ignore
1317 return
1319 for row in model:
1320 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
1321 new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size, channel_url, self.cover_cache)
1322 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
1324 def save_episode_as_file( self, url, *args):
1325 episode = self.active_channel.find_episode(url)
1327 if episode.was_downloaded(and_exists=True):
1328 folder = self.folder_for_saving_episodes
1329 copy_from = episode.local_filename(create=False)
1330 assert copy_from is not None
1331 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=episode.sync_filename(), dst_directory=folder)
1332 self.folder_for_saving_episodes = folder
1334 def copy_episode_bluetooth(self, url, *args):
1335 episode = self.active_channel.find_episode(url)
1337 if not episode.was_downloaded(and_exists=True):
1338 log('Cannot copy episode via bluetooth (does not exist!)', sender=self)
1340 filename = episode.local_filename(create=False)
1341 assert filename is not None
1343 if gl.config.bluetooth_use_device_address:
1344 device = gl.config.bluetooth_device_address
1345 else:
1346 device = None
1348 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
1349 (base, ext) = os.path.splitext(filename)
1350 if not destfile.endswith(ext):
1351 destfile += ext
1353 if gl.config.bluetooth_use_converter:
1354 title = _('Converting file')
1355 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
1356 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1357 dlg.set_title(title)
1358 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1359 dlg.show_all()
1360 else:
1361 dlg = None
1363 def convert_and_send_thread(filename, destfile, device, dialog, notify):
1364 if gl.config.bluetooth_use_converter:
1365 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
1366 result = p.wait()
1367 if dialog is not None:
1368 dialog.destroy()
1369 else:
1370 try:
1371 shutil.copyfile(filename, destfile)
1372 result = 0
1373 except:
1374 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1375 result = 1
1377 if result == 0 or not os.path.exists(destfile):
1378 util.bluetooth_send_file(destfile, device)
1379 else:
1380 notify(_('Error converting file.'), _('Bluetooth file transfer'))
1381 util.delete_file(destfile)
1383 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
1385 def treeview_button_savepos(self, treeview, event):
1386 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1387 self.treeview_available_buttonpress = (event.x, event.y)
1388 return True
1390 def treeview_channels_button_released(self, treeview, event):
1391 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1392 selection = self.treeChannels.get_selection()
1393 pathatpos = self.treeChannels.get_path_at_pos(int(event.x), int(event.y))
1394 if self.currently_updating:
1395 log('do not handle press while updating', sender=self)
1396 return True
1397 if pathatpos is None:
1398 return False
1399 else:
1400 ydistance = int(abs(event.y-self.treeview_channels_buttonpress[1]))
1401 xdistance = int(event.x-self.treeview_channels_buttonpress[0])
1402 if ydistance < 30:
1403 (path, column, x, y) = pathatpos
1404 selection.select_path(path)
1405 self.treeChannels.set_cursor(path)
1406 self.treeChannels.grab_focus()
1407 return True
1409 def treeview_button_pressed( self, treeview, event):
1410 global WEB_BROWSER_ICON
1412 if gpodder.interface == gpodder.MAEMO:
1413 ydistance = int(abs(event.y-self.treeview_available_buttonpress[1]))
1414 xdistance = int(event.x-self.treeview_available_buttonpress[0])
1416 selection = self.treeAvailable.get_selection()
1417 pathatpos = self.treeAvailable.get_path_at_pos(int(event.x), int(event.y))
1418 if pathatpos is None:
1419 # No item at the current cursor position
1420 return False
1421 elif ydistance < 30:
1422 # Item under the cursor, and no scrolling done
1423 (path, column, x, y) = pathatpos
1424 selection.select_path(path)
1425 self.treeAvailable.set_cursor(path)
1426 self.treeAvailable.grab_focus()
1427 if gl.config.maemo_enable_gestures and xdistance > 70:
1428 self.on_treeAvailable_row_activated(self.itemPlaySelected)
1429 return True
1430 elif gl.config.maemo_enable_gestures and xdistance < -70:
1431 self.on_treeAvailable_row_activated(self.treeAvailable)
1432 return True
1433 else:
1434 # Scrolling has been done
1435 return True
1437 # Use right-click for the Desktop version and left-click for Maemo
1438 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1439 (event.button == 3 and gpodder.interface == gpodder.GUI):
1440 ( x, y ) = ( int(event.x), int(event.y) )
1441 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1443 paths = []
1445 # Did the user right-click into a selection?
1446 selection = self.treeAvailable.get_selection()
1447 if selection.count_selected_rows() and path:
1448 ( model, paths ) = selection.get_selected_rows()
1449 if path not in paths:
1450 # We have right-clicked, but not into the
1451 # selection, assume we don't want to operate
1452 # on the selection
1453 paths = []
1455 # No selection or right click not in selection:
1456 # Select the single item where we clicked
1457 if not len( paths) and path:
1458 treeview.grab_focus()
1459 treeview.set_cursor( path, column, 0)
1461 ( model, paths ) = ( treeview.get_model(), [ path ] )
1463 # We did not find a selection, and the user didn't
1464 # click on an item to select -- don't show the menu
1465 if not len( paths):
1466 return True
1468 first_url = model.get_value( model.get_iter( paths[0]), 0)
1469 episode = db.load_episode(first_url)
1471 menu = gtk.Menu()
1473 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1475 if open_instead_of_play:
1476 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1477 else:
1478 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1480 item.set_sensitive(can_play)
1481 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolPlay))
1482 menu.append(self.set_finger_friendly(item))
1484 if not can_cancel:
1485 item = gtk.ImageMenuItem(_('Download'))
1486 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1487 item.set_sensitive(can_download)
1488 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolDownload))
1489 menu.append(self.set_finger_friendly(item))
1490 else:
1491 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1492 item.connect('activate', lambda w: self.on_treeDownloads_row_activated(self.toolCancel))
1493 menu.append(self.set_finger_friendly(item))
1495 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1496 item.set_sensitive(can_delete and not episode['is_locked'])
1497 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1498 menu.append(self.set_finger_friendly(item))
1500 # FIXME - fix the following block
1501 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1502 item = gtk.ImageMenuItem(_('Do not download'))
1503 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1504 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1505 menu.append(self.set_finger_friendly(item))
1506 elif episode['state'] == db.STATE_NORMAL and can_download:
1507 item = gtk.ImageMenuItem(_('Mark as new'))
1508 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1509 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1510 menu.append(self.set_finger_friendly(item))
1512 # Ok, this probably makes sense to only display for downloaded files
1513 if can_play and not can_download:
1514 menu.append( gtk.SeparatorMenuItem())
1515 item = gtk.ImageMenuItem(_('Save to disk'))
1516 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1517 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1518 menu.append(self.set_finger_friendly(item))
1519 if gl.bluetooth_available:
1520 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1521 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1522 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1523 menu.append(self.set_finger_friendly(item))
1524 if can_transfer:
1525 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1526 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1527 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1528 menu.append(self.set_finger_friendly(item))
1530 if can_play:
1531 menu.append( gtk.SeparatorMenuItem())
1532 is_played = episode['is_played']
1533 if is_played:
1534 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1535 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1536 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1537 menu.append(self.set_finger_friendly(item))
1538 else:
1539 item = gtk.ImageMenuItem(_('Mark as played'))
1540 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1541 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1542 menu.append(self.set_finger_friendly(item))
1544 is_locked = episode['is_locked']
1545 if is_locked:
1546 item = gtk.ImageMenuItem(_('Allow deletion'))
1547 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1548 item.connect('activate', self.on_item_toggle_lock_activate)
1549 menu.append(self.set_finger_friendly(item))
1550 else:
1551 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1552 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1553 item.connect('activate', self.on_item_toggle_lock_activate)
1554 menu.append(self.set_finger_friendly(item))
1556 menu.append(gtk.SeparatorMenuItem())
1557 # Single item, add episode information menu item
1558 episode_url = model.get_value(model.get_iter(paths[0]), 0)
1559 item = gtk.ImageMenuItem(_('Episode details'))
1560 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1561 item.set_sensitive(len(paths) == 1)
1562 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1563 menu.append(self.set_finger_friendly(item))
1565 episode = self.active_channel.find_episode(episode_url)
1566 # If we have it, also add episode website link
1567 if len(paths) == 1 and episode and episode.link and episode.link != episode.url:
1568 item = gtk.ImageMenuItem(_('Visit website'))
1569 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1570 item.connect('activate', lambda w: util.open_website(episode.link))
1571 menu.append(self.set_finger_friendly(item))
1573 if gpodder.interface == gpodder.MAEMO:
1574 # Because we open the popup on left-click for Maemo,
1575 # we also include a non-action to close the menu
1576 menu.append(gtk.SeparatorMenuItem())
1577 item = gtk.ImageMenuItem(_('Close this menu'))
1578 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1579 menu.append(self.set_finger_friendly(item))
1581 menu.show_all()
1582 # Disable tooltips while we are showing the menu, so
1583 # the tooltip will not appear over the menu
1584 self.episode_list_can_tooltip = False
1585 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1586 menu.popup( None, None, None, event.button, event.time)
1588 return True
1590 def set_title(self, new_title):
1591 self.default_title = new_title
1592 self.gPodder.set_title(new_title)
1594 def update_selected_episode_list_icons(self):
1596 Updates the status icons in the episode list
1598 selection = self.treeAvailable.get_selection()
1599 (model, paths) = selection.get_selected_rows()
1600 for path in paths:
1601 iter = model.get_iter(path)
1602 self.active_channel.iter_set_downloading_columns(model, iter, downloading=self.episode_is_downloading)
1604 def update_episode_list_icons(self, urls):
1606 Updates the status icons in the episode list
1607 Only update the episodes that have an URL in
1608 the "urls" iterable object (e.g. a list of URLs)
1610 if self.active_channel is None or not urls:
1611 return
1613 model = self.treeAvailable.get_model()
1614 if model is None:
1615 return
1617 for url in urls:
1618 if url in self.url_path_mapping:
1619 path = (self.url_path_mapping[url],)
1620 self.active_channel.iter_set_downloading_columns(model, model.get_iter(path), downloading=self.episode_is_downloading)
1622 def playback_episode(self, episode, stream=False):
1623 if gpodder.interface == gpodder.MAEMO:
1624 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Opening %s') % saxutils.escape(episode.title))
1625 def destroy_banner_later(banner):
1626 banner.destroy()
1627 return False
1628 gobject.timeout_add(5000, destroy_banner_later, banner)
1629 (success, application) = gl.playback_episode(episode, stream)
1630 if not success:
1631 self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), ))
1632 self.update_selected_episode_list_icons()
1633 self.updateComboBox(only_selected_channel=True)
1635 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1636 if model is None:
1637 return True
1639 key = key.lower()
1641 # columns, as defined in libpodcasts' get model method
1642 # 1 = episode title, 7 = description
1643 columns = (1, 7)
1645 for column in columns:
1646 value = model.get_value( iter, column).lower()
1647 if value.find( key) != -1:
1648 return False
1650 return True
1652 def change_menu_item(self, menuitem, icon=None, label=None):
1653 if icon is not None:
1654 menuitem.set_property('stock-id', icon)
1655 if label is not None:
1656 menuitem.label = label
1658 def play_or_download(self):
1659 if self.wNotebook.get_current_page() > 0:
1660 return
1662 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1663 ( is_played, is_locked ) = (False,)*2
1665 open_instead_of_play = False
1667 selection = self.treeAvailable.get_selection()
1668 if selection.count_selected_rows() > 0:
1669 (model, paths) = selection.get_selected_rows()
1671 for path in paths:
1672 url = model.get_value( model.get_iter( path), 0)
1674 episode = self.active_channel.find_episode(url)
1676 if episode.file_type() not in ('audio', 'video'):
1677 open_instead_of_play = True
1679 if episode.was_downloaded():
1680 can_play = episode.was_downloaded(and_exists=True)
1681 can_delete = True
1682 is_played = episode.is_played
1683 is_locked = episode.is_locked
1684 if not can_play:
1685 can_download = True
1686 else:
1687 if self.episode_is_downloading(episode):
1688 can_cancel = True
1689 else:
1690 can_download = True
1692 can_download = can_download and not can_cancel
1693 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1694 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1696 if open_instead_of_play:
1697 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1698 can_transfer = False
1699 else:
1700 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1702 self.toolPlay.set_sensitive( can_play)
1703 self.toolDownload.set_sensitive( can_download)
1704 self.toolTransfer.set_sensitive( can_transfer)
1705 self.toolCancel.set_sensitive( can_cancel)
1707 self.item_cancel_download.set_sensitive(can_cancel)
1708 self.itemDownloadSelected.set_sensitive(can_download)
1709 self.itemOpenSelected.set_sensitive(can_play)
1710 self.itemPlaySelected.set_sensitive(can_play)
1711 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1712 self.item_toggle_played.set_sensitive(can_play)
1713 self.item_toggle_lock.set_sensitive(can_play)
1715 self.itemOpenSelected.set_visible(open_instead_of_play)
1716 self.itemPlaySelected.set_visible(not open_instead_of_play)
1718 if can_play:
1719 if is_played:
1720 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1721 else:
1722 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1723 if is_locked:
1724 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1725 else:
1726 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1728 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1730 def on_cbMaxDownloads_toggled(self, widget, *args):
1731 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1733 def on_cbLimitDownloads_toggled(self, widget, *args):
1734 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1736 def episode_new_status_changed(self, urls):
1737 self.updateComboBox()
1738 self.update_episode_list_icons(urls)
1740 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1741 selection = self.treeChannels.get_selection()
1742 (model, iter) = selection.get_selected()
1744 if only_selected_channel:
1745 # very cheap! only update selected channel
1746 if iter and self.active_channel is not None:
1747 update_channel_model_by_iter(model, iter,
1748 self.active_channel, self.channel_colors,
1749 self.cover_cache,
1750 gl.config.podcast_list_icon_size,
1751 gl.config.podcast_list_icon_size)
1752 elif not self.channel_list_changed:
1753 # we can keep the model, but have to update some
1754 if only_these_urls is None:
1755 # still cheaper than reloading the whole list
1756 iter = model.get_iter_first()
1757 while iter is not None:
1758 (index,) = model.get_path(iter)
1759 update_channel_model_by_iter(model, iter,
1760 self.channels[index], self.channel_colors,
1761 self.cover_cache,
1762 gl.config.podcast_list_icon_size,
1763 gl.config.podcast_list_icon_size)
1764 iter = model.iter_next(iter)
1765 else:
1766 # ok, we got a bunch of urls to update
1767 for url in only_these_urls:
1768 if url in self.channel_url_path_mapping:
1769 index = self.channel_url_path_mapping[url]
1770 path = (index,)
1771 iter = model.get_iter(path)
1772 update_channel_model_by_iter(model, iter,
1773 self.channels[index], self.channel_colors,
1774 self.cover_cache,
1775 gl.config.podcast_list_icon_size,
1776 gl.config.podcast_list_icon_size)
1777 else:
1778 if model and iter and selected_url is None:
1779 # Get the URL of the currently-selected podcast
1780 selected_url = model.get_value(iter, 0)
1782 (model, urls) = channels_to_model(self.channels,
1783 self.channel_colors, self.cover_cache,
1784 gl.config.podcast_list_icon_size,
1785 gl.config.podcast_list_icon_size)
1787 self.channel_url_path_mapping = dict(zip(urls, range(len(urls))))
1788 self.treeChannels.set_model(model)
1790 try:
1791 selected_path = (0,)
1792 # Find the previously-selected URL in the new
1793 # model if we have an URL (else select first)
1794 if selected_url is not None:
1795 pos = model.get_iter_first()
1796 while pos is not None:
1797 url = model.get_value(pos, 0)
1798 if url == selected_url:
1799 selected_path = model.get_path(pos)
1800 break
1801 pos = model.iter_next(pos)
1803 self.treeChannels.get_selection().select_path(selected_path)
1804 except:
1805 log( 'Cannot set selection on treeChannels', sender = self)
1806 self.on_treeChannels_cursor_changed( self.treeChannels)
1807 self.channel_list_changed = False
1809 def episode_is_downloading(self, episode):
1810 """Returns True if the given episode is being downloaded at the moment"""
1811 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1813 def updateTreeView(self):
1814 if self.channels and self.active_channel is not None:
1815 if gpodder.interface == gpodder.MAEMO:
1816 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes for %s') % saxutils.escape(self.active_channel.title))
1817 else:
1818 banner = None
1819 def thread_func(self, banner, active_channel):
1820 (model, urls) = self.active_channel.get_tree_model(self.episode_is_downloading)
1821 mapping = dict(zip(urls, range(len(urls))))
1822 def update_gui_with_new_model(self, channel, model, urls, mapping, banner):
1823 if self.active_channel is not None and channel is not None:
1824 log('%s <=> %s', self.active_channel.title, channel.title, sender=self)
1825 if self.active_channel == channel:
1826 self.treeAvailable.set_model(model)
1827 self.url_path_mapping = mapping
1828 self.treeAvailable.columns_autosize()
1829 self.play_or_download()
1830 if banner is not None:
1831 banner.destroy()
1832 self.currently_updating = False
1833 return False
1834 gobject.idle_add(lambda: update_gui_with_new_model(self, active_channel, model, urls, mapping, banner))
1835 self.currently_updating = True
1836 Thread(target=thread_func, args=[self, banner, self.active_channel]).start()
1837 else:
1838 model = self.treeAvailable.get_model()
1839 if model is not None:
1840 model.clear()
1842 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1843 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1845 dnd_channel = None
1846 if path is not None:
1847 model = self.treeChannels.get_model()
1848 iter = model.get_iter(path)
1849 url = model.get_value(iter, 0)
1850 for channel in self.channels:
1851 if channel.url == url:
1852 dnd_channel = channel
1853 break
1855 result = sel.data
1856 rl = result.strip().lower()
1857 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1858 services.cover_downloader.replace_cover(dnd_channel, result)
1859 else:
1860 self.add_new_channel(result)
1862 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1863 result = util.normalize_feed_url(result)
1864 (scheme, rest) = result.split('://', 1)
1866 if not result:
1867 cute_scheme = saxutils.escape(scheme)+'://'
1868 title = _('%s URLs are not supported') % cute_scheme
1869 message = _('gPodder does not understand the URL you supplied.')
1870 self.show_message( message, title)
1871 return
1873 for old_channel in self.channels:
1874 if old_channel.url == result:
1875 log( 'Channel already exists: %s', result)
1876 # Select the existing channel in combo box
1877 for i in range( len( self.channels)):
1878 if self.channels[i] == old_channel:
1879 self.treeChannels.get_selection().select_path( (i,))
1880 self.on_treeChannels_cursor_changed(self.treeChannels)
1881 break
1882 self.show_message( _('You have already subscribed to this podcast: %s') % (
1883 saxutils.escape( old_channel.title), ), _('Already added'))
1884 return
1886 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1887 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1888 waitdlg.set_title(_('Downloading episode list'))
1889 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1890 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1891 waitpb = gtk.ProgressBar()
1892 if block:
1893 waitdlg.vbox.add(waitpb)
1894 waitdlg.show_all()
1895 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1897 self.entryAddChannel.set_text(_('Downloading feed...'))
1898 self.entryAddChannel.set_sensitive(False)
1899 self.btnAddChannel.set_sensitive(False)
1900 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1901 thread = Thread( target=self.add_new_channel_proc, args=args )
1902 thread.start()
1904 while block and thread.isAlive():
1905 while gtk.events_pending():
1906 gtk.main_iteration( False)
1907 waitpb.pulse()
1908 time.sleep(0.1)
1911 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1912 log( 'Adding new channel: %s', url)
1913 channel = error = None
1914 try:
1915 channel = PodcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1916 except HTTPAuthError, e:
1917 error = e
1918 except Exception, e:
1919 log('Error in PodcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1921 util.idle_add( callback, channel, url, error, *callback_args )
1923 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1924 if channel is not None:
1925 self.channels.append( channel)
1926 self.channel_list_changed = True
1927 save_channels( self.channels)
1928 if not quiet:
1929 # download changed channels and select the new episode in the UI afterwards
1930 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1932 try:
1933 (username, password) = util.username_password_from_url(url)
1934 except ValueError, ve:
1935 self.show_message(_('The following error occured while trying to get authentication data from the URL:') + '\n\n' + ve.message, _('Error getting authentication data'))
1936 (username, password) = (None, None)
1937 log('Error getting authentication data from URL: %s', url, traceback=True)
1939 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')):
1940 channel.username = username
1941 channel.password = password
1942 log('Saving authentication data for episode downloads..', sender = self)
1943 channel.save()
1944 # We need to update the channel list otherwise the authentication
1945 # data won't show up in the channel editor.
1946 # TODO: Only updated the newly added feed to save some cpu cycles
1947 self.channels = load_channels()
1948 self.channel_list_changed = True
1950 if ask_download_new:
1951 new_episodes = channel.get_new_episodes(downloading=self.episode_is_downloading)
1952 if len(new_episodes):
1953 self.new_episodes_show(new_episodes)
1955 elif isinstance( error, HTTPAuthError ):
1956 response, auth_tokens = self.UsernamePasswordDialog(
1957 _('Feed requires authentication'), _('Please enter your username and password.'))
1959 if response:
1960 self.add_new_channel( url, authentication_tokens=auth_tokens )
1962 else:
1963 # Ok, the URL is not a channel, or there is some other
1964 # error - let's see if it's a web page or OPML file...
1965 try:
1966 data = urllib2.urlopen(url).read().lower()
1967 if '</opml>' in data:
1968 # This looks like an OPML feed
1969 self.on_item_import_from_file_activate(None, url)
1971 elif '</html>' in data:
1972 # This looks like a web page
1973 title = _('The URL is a website')
1974 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.)')
1975 if self.show_confirmation(message, title):
1976 util.open_website(url)
1978 except Exception, e:
1979 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1981 title = _('Error adding podcast')
1982 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1983 self.show_message( message, title)
1985 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1986 self.entryAddChannel.set_sensitive(True)
1987 self.btnAddChannel.set_sensitive(True)
1988 self.update_podcasts_tab()
1989 waitdlg.destroy()
1992 def update_feed_cache_finish_callback(self, channels=None,
1993 notify_no_new_episodes=False, select_url_afterwards=None):
1995 db.commit()
1997 self.updating_feed_cache = False
1998 if gpodder.interface == gpodder.MAEMO:
1999 self.btnCancelFeedUpdate.show()
2000 self.itemUpdate.set_sensitive(True)
2001 self.itemUpdateChannel.set_sensitive(True)
2003 # If we want to select a specific podcast (via its URL)
2004 # after the update, we give it to updateComboBox here to
2005 # select exactly this podcast after updating the view
2006 self.updateComboBox(selected_url=select_url_afterwards)
2008 self.channels = load_channels()
2009 self.channel_list_changed = True
2010 self.updateComboBox()
2012 episodes = self.get_new_episodes()
2014 if self.tray_icon:
2015 self.tray_icon.set_status(None)
2016 if self.minimized:
2017 # Determine new episodes that we have not yet announced
2018 new_episodes = [episode for episode in episodes \
2019 if episode not in self.already_notified_new_episodes]
2020 self.already_notified_new_episodes.extend(new_episodes)
2022 if len(new_episodes) == 0:
2023 if notify_no_new_episodes and self.tray_icon is not None:
2024 msg = _('No new episodes available for download')
2025 self.tray_icon.send_notification(msg)
2026 else:
2027 if len(new_episodes) == 1:
2028 title = _('gPodder has found %s') % (_('one new episode:'),)
2029 else:
2030 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
2031 message = self.tray_icon.format_episode_list([e.title for e in new_episodes])
2033 #auto download new episodes
2034 if gl.config.auto_download_when_minimized:
2035 message += '\n<i>(%s...)</i>' % _('downloading')
2036 self.download_episode_list(new_episodes)
2037 self.tray_icon.send_notification(message, title)
2039 if len(episodes) == 0 or self.feed_cache_update_cancelled:
2040 self.pbFeedUpdate.set_fraction(1.0)
2041 if self.feed_cache_update_cancelled:
2042 self.pbFeedUpdate.set_text(_('Update has been cancelled'))
2043 else:
2044 self.pbFeedUpdate.set_text(_('No new episodes'))
2045 self.feed_cache_update_cancelled = True
2046 self.btnCancelFeedUpdate.show()
2047 self.btnCancelFeedUpdate.set_sensitive(True)
2048 if gpodder.interface == gpodder.MAEMO:
2049 # btnCancelFeedUpdate is a ToolButton on Maemo
2050 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2051 else:
2052 # btnCancelFeedUpdate is a normal gtk.Button
2053 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2054 else:
2055 if self.minimized and gl.config.auto_download_when_minimized:
2056 new_episodes = [episode for episode in episodes if episode not in self.already_notified_new_episodes]
2057 self.already_notified_new_episodes.extend(new_episodes)
2058 if len(new_episodes) > 0:
2059 self.download_episode_list(new_episodes)
2060 else:
2061 # open the episodes selection dialog
2062 self.new_episodes_show(episodes)
2064 def update_feed_cache_callback(self, progressbar, title, position, count):
2065 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
2066 progressbar.set_text(progression)
2067 if self.tray_icon:
2068 self.tray_icon.set_status(
2069 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
2070 if count > 0:
2071 progressbar.set_fraction(float(position)/float(count))
2073 def update_feed_cache_proc( self, channel, total_channels, semaphore,
2074 callback_proc, finish_proc):
2076 semaphore.acquire()
2077 if not self.feed_cache_update_cancelled:
2078 try:
2079 channel.update()
2080 except:
2081 log('Darn SQLite LOCK!', sender=self, traceback=True)
2083 # By the time we get here the update may have already been cancelled
2084 if not self.feed_cache_update_cancelled:
2085 callback_proc(channel.title, self.updated_feeds, total_channels)
2087 self.updated_feeds += 1
2088 self.treeview_channel_set_color( channel, 'default' )
2089 channel.update_flag = False
2091 semaphore.release()
2092 if self.updated_feeds == total_channels:
2093 finish_proc()
2095 def on_btnCancelFeedUpdate_clicked(self, widget):
2096 if self.feed_cache_update_cancelled:
2097 if gpodder.interface == gpodder.MAEMO:
2098 self.btnUpdateSelectedFeed.show()
2099 self.toolFeedUpdateProgress.hide()
2100 self.btnCancelFeedUpdate.hide()
2101 self.btnCancelFeedUpdate.set_is_important(False)
2102 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2103 self.toolbarSpacer.set_expand(True)
2104 self.toolbarSpacer.set_draw(False)
2105 else:
2106 self.hboxUpdateFeeds.hide()
2107 self.btnUpdateFeeds.show()
2108 else:
2109 self.pbFeedUpdate.set_text(_('Cancelling, please wait...'))
2110 self.feed_cache_update_cancelled = True
2111 self.btnCancelFeedUpdate.set_sensitive(False)
2113 def update_feed_cache(self, channels=None, force_update=True,
2114 notify_no_new_episodes=False, select_url_afterwards=None):
2116 if self.updating_feed_cache:
2117 return
2119 if not force_update:
2120 self.channels = load_channels()
2121 self.channel_list_changed = True
2122 self.updateComboBox()
2123 return
2125 self.updating_feed_cache = True
2126 self.itemUpdate.set_sensitive(False)
2127 self.itemUpdateChannel.set_sensitive(False)
2129 if self.tray_icon:
2130 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2132 if channels is None:
2133 channels = self.channels
2135 if len(channels) == 1:
2136 text = _('Updating "%s"...') % channels[0].title
2137 else:
2138 text = _('Updating %d feeds...') % len(channels)
2139 self.pbFeedUpdate.set_text(text)
2140 self.pbFeedUpdate.set_fraction(0)
2142 # let's get down to business..
2143 callback_proc = lambda title, pos, count: util.idle_add(
2144 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
2145 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
2146 channels, notify_no_new_episodes, select_url_afterwards )
2148 self.updated_feeds = 0
2149 self.feed_cache_update_cancelled = False
2150 self.btnCancelFeedUpdate.show()
2151 self.btnCancelFeedUpdate.set_sensitive(True)
2152 if gpodder.interface == gpodder.MAEMO:
2153 self.toolbarSpacer.set_expand(False)
2154 self.toolbarSpacer.set_draw(True)
2155 self.btnUpdateSelectedFeed.hide()
2156 self.toolFeedUpdateProgress.show_all()
2157 else:
2158 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2159 self.hboxUpdateFeeds.show_all()
2160 self.btnUpdateFeeds.hide()
2161 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
2163 for channel in channels:
2164 self.treeview_channel_set_color( channel, 'updating' )
2165 channel.update_flag = True
2166 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
2167 thread = Thread( target = self.update_feed_cache_proc, args = args)
2168 thread.start()
2170 def treeview_channel_set_color( self, channel, color ):
2171 if self.treeChannels.get_model():
2172 if color in self.channel_colors:
2173 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
2174 else:
2175 self.treeChannels.get_model().set(channel.iter, 8, color)
2177 def on_gPodder_delete_event(self, widget, *args):
2178 """Called when the GUI wants to close the window
2179 Displays a confirmation dialog (and closes/hides gPodder)
2182 downloading = self.download_status_manager.are_downloads_in_progress()
2184 # Only iconify if we are using the window's "X" button,
2185 # but not when we are using "Quit" in the menu or toolbar
2186 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
2187 self.iconify_main_window()
2188 elif gl.config.on_quit_ask or downloading:
2189 if gpodder.interface == gpodder.MAEMO:
2190 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2191 if result:
2192 self.close_gpodder()
2193 else:
2194 return True
2195 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2196 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2197 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2199 title = _('Quit gPodder')
2200 if downloading:
2201 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2202 else:
2203 message = _('Do you really want to quit gPodder now?')
2205 dialog.set_title(title)
2206 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2207 if not downloading:
2208 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2209 dialog.vbox.pack_start(cb_ask)
2210 cb_ask.show_all()
2212 result = dialog.run()
2213 dialog.destroy()
2215 if result == gtk.RESPONSE_CLOSE:
2216 if not downloading and cb_ask.get_active() == True:
2217 gl.config.on_quit_ask = False
2218 self.close_gpodder()
2219 else:
2220 self.close_gpodder()
2222 return True
2224 def close_gpodder(self):
2225 """ clean everything and exit properly
2227 if self.channels:
2228 if save_channels(self.channels):
2229 if gl.config.my_gpodder_autoupload:
2230 log('Uploading to my.gpodder.org on close', sender=self)
2231 util.idle_add(self.on_upload_to_mygpo, None)
2232 else:
2233 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
2235 self.gPodder.hide()
2237 if self.tray_icon is not None:
2238 self.tray_icon.set_visible(False)
2240 # Notify all tasks to to carry out any clean-up actions
2241 self.download_status_manager.tell_all_tasks_to_quit()
2243 while gtk.events_pending():
2244 gtk.main_iteration(False)
2246 db.close()
2248 self.quit()
2249 sys.exit(0)
2251 def get_old_episodes(self):
2252 episodes = []
2253 for channel in self.channels:
2254 for episode in channel.get_downloaded_episodes():
2255 if episode.is_old() and not episode.is_locked and episode.is_played:
2256 episodes.append(episode)
2257 return episodes
2259 def for_each_selected_episode_url( self, callback):
2260 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
2261 for path in paths:
2262 url = model.get_value( model.get_iter( path), 0)
2263 try:
2264 callback( url)
2265 except Exception, e:
2266 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
2268 self.update_selected_episode_list_icons()
2269 self.updateComboBox(only_selected_channel=True)
2270 db.commit()
2272 def delete_episode_list( self, episodes, confirm = True):
2273 if len(episodes) == 0:
2274 return
2276 if len(episodes) == 1:
2277 message = _('Do you really want to delete this episode?')
2278 else:
2279 message = _('Do you really want to delete %d episodes?') % len(episodes)
2281 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
2282 return
2284 episode_urls = set()
2285 channel_urls = set()
2286 for episode in episodes:
2287 log('Deleting episode: %s', episode.title, sender = self)
2288 episode.delete_from_disk()
2289 episode_urls.add(episode.url)
2290 channel_urls.add(episode.channel.url)
2292 # Episodes have been deleted - persist the database
2293 db.commit()
2295 #self.download_status_updated(episode_urls, channel_urls)
2297 def on_itemRemoveOldEpisodes_activate( self, widget):
2298 columns = (
2299 ('title_and_description', None, None, _('Episode')),
2300 ('channel_prop', None, None, _('Podcast')),
2301 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2302 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2303 ('played_prop', None, None, _('Status')),
2304 ('age_prop', None, None, _('Downloaded')),
2307 selection_buttons = {
2308 _('Select played'): lambda episode: episode.is_played,
2309 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
2312 instructions = _('Select the episodes you want to delete from your hard disk.')
2314 episodes = []
2315 selected = []
2316 for channel in self.channels:
2317 for episode in channel.get_downloaded_episodes():
2318 if not episode.is_locked:
2319 episodes.append(episode)
2320 selected.append(episode.is_played)
2322 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
2323 episodes = episodes, selected = selected, columns = columns, \
2324 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2325 selection_buttons = selection_buttons)
2327 def mark_selected_episodes_new(self):
2328 callback = lambda url: self.active_channel.find_episode(url).mark_new()
2329 self.for_each_selected_episode_url(callback)
2331 def mark_selected_episodes_old(self):
2332 callback = lambda url: self.active_channel.find_episode(url).mark_old()
2333 self.for_each_selected_episode_url(callback)
2335 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2336 if toggle:
2337 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
2338 else:
2339 callback = lambda url: db.mark_episode(url, is_played=new_value)
2341 self.for_each_selected_episode_url(callback)
2343 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2344 if toggle:
2345 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
2346 else:
2347 callback = lambda url: db.mark_episode(url, is_locked=new_value)
2349 self.for_each_selected_episode_url(callback)
2351 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2352 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2353 db.update_channel_lock(self.active_channel)
2355 if self.active_channel.channel_is_locked:
2356 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2357 else:
2358 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2360 for episode in self.active_channel.get_all_episodes():
2361 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
2363 self.updateComboBox(only_selected_channel=True)
2365 def on_item_email_subscriptions_activate(self, widget):
2366 if not self.channels:
2367 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
2368 elif not gl.send_subscriptions():
2369 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
2371 def on_itemUpdateChannel_activate(self, widget=None):
2372 self.update_feed_cache(channels=[self.active_channel,])
2374 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
2375 restore_from = can_restore_from_opml()
2377 if self.channels:
2378 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
2379 elif restore_from is not None:
2380 title = _('Database upgrade required')
2381 message = _('gPodder is now using a new (much faster) database backend and needs to convert your current data. This can take some time. Start the conversion now?')
2382 if self.show_confirmation(message, title):
2383 add_callback = lambda url: self.add_new_channel(url, False, True)
2384 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
2385 w.set_has_separator(False)
2386 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
2387 w.set_default_size(500, -1)
2388 pb = gtk.ProgressBar()
2389 l = gtk.Label()
2390 l.set_padding(6, 3)
2391 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
2392 l.set_alignment(0.0, 0.5)
2393 w.vbox.pack_start(l)
2394 l = gtk.Label()
2395 l.set_padding(6, 3)
2396 l.set_alignment(0.0, 0.5)
2397 l.set_text(_('Please wait while your settings are converted.'))
2398 w.vbox.pack_start(l)
2399 w.vbox.pack_start(pb)
2400 lb = gtk.Label()
2401 lb.set_ellipsize(pango.ELLIPSIZE_END)
2402 lb.set_alignment(0.0, 0.5)
2403 lb.set_padding(6, 6)
2404 w.vbox.pack_start(lb)
2406 def set_pb_status(pb, lb, fraction, text):
2407 pb.set_fraction(float(fraction)/100.0)
2408 pb.set_text('%.0f %%' % fraction)
2409 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
2410 while gtk.events_pending():
2411 gtk.main_iteration(False)
2412 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
2413 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
2414 w.show_all()
2415 start = datetime.datetime.now()
2416 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
2417 # Refresh the view with the updated episodes
2418 self.updateComboBox()
2419 time_taken = str(datetime.datetime.now()-start)
2420 status_callback(100.0, _('Migration finished in %s') % time_taken)
2421 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
2422 w.run()
2423 w.destroy()
2424 else:
2425 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
2427 def download_episode_list_paused(self, episodes):
2428 self.download_episode_list(episodes, True)
2430 def download_episode_list(self, episodes, add_paused=False):
2431 for episode in episodes:
2432 log('Downloading episode: %s', episode.title, sender = self)
2433 if not episode.was_downloaded(and_exists=True):
2434 task_exists = False
2435 for task in self.download_tasks_seen:
2436 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2437 self.download_queue_manager.add_task(task)
2438 task_exists = True
2439 continue
2441 if task_exists:
2442 continue
2444 task = download.DownloadTask(episode)
2445 if add_paused:
2446 task.status = task.PAUSED
2447 self.download_queue_manager.add_resumed_task(task)
2448 else:
2449 self.download_queue_manager.add_task(task)
2451 def new_episodes_show(self, episodes):
2452 columns = (
2453 ('title_and_description', None, None, _('Episode')),
2454 ('channel_prop', None, None, _('Podcast')),
2455 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2456 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2459 instructions = _('Select the episodes you want to download now.')
2461 self.feed_cache_update_cancelled = True
2462 self.on_btnCancelFeedUpdate_clicked(self.btnCancelFeedUpdate)
2464 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
2465 episodes=episodes, columns=columns, selected_default=True, \
2466 stock_ok_button = 'gpodder-download', \
2467 callback=self.download_episode_list, \
2468 remove_callback=lambda e: e.mark_old(), \
2469 remove_action=_('Never download'), \
2470 remove_finished=self.episode_new_status_changed)
2472 def on_itemDownloadAllNew_activate(self, widget, *args):
2473 new_episodes = self.get_new_episodes()
2474 if len(new_episodes):
2475 self.new_episodes_show(new_episodes)
2476 else:
2477 msg = _('No new episodes available for download')
2478 if self.tray_icon is not None and self.minimized:
2479 self.tray_icon.send_notification(msg)
2480 else:
2481 self.show_message(msg, _('No new episodes'))
2483 def get_new_episodes(self, channels=None):
2484 if channels is None:
2485 channels = self.channels
2486 episodes = []
2487 for channel in channels:
2488 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2489 episodes.append(episode)
2491 return episodes
2493 def get_all_episodes(self, exclude_nonsignificant=True ):
2494 """'exclude_nonsignificant' will exclude non-downloaded episodes
2495 and all episodes from channels that are set to skip when syncing"""
2496 episode_list = []
2497 for channel in self.channels:
2498 if not channel.sync_to_devices and exclude_nonsignificant:
2499 log('Skipping channel: %s', channel.title, sender=self)
2500 continue
2501 for episode in channel.get_all_episodes():
2502 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
2503 episode_list.append(episode)
2504 return episode_list
2506 def ipod_delete_played(self, device):
2507 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
2508 episodes_on_device = device.get_all_tracks()
2509 for local_episode in all_episodes:
2510 device_episode = device.episode_on_device(local_episode)
2511 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2512 or local_episode.state == db.STATE_DELETED ):
2513 log("mp3_player_delete_played: removing %s" % device_episode.title)
2514 device.remove_track(device_episode)
2516 def on_sync_to_ipod_activate(self, widget, episodes=None):
2517 # make sure gpod is available before even trying to sync
2518 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2519 title = _('Cannot Sync To iPod')
2520 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2521 self.notification( message, title )
2522 return
2523 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2524 title = _('Cannot sync to MTP device')
2525 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2526 self.notification( message, title )
2527 return
2529 device = sync.open_device()
2530 device.register( 'post-done', self.sync_to_ipod_completed )
2532 if device is None:
2533 title = _('No device configured')
2534 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2535 self.notification(message, title)
2536 return
2538 if not device.open():
2539 title = _('Cannot open device')
2540 message = _('There has been an error opening your device.')
2541 self.notification(message, title)
2542 return
2544 if gl.config.device_type == 'ipod':
2545 #update played episodes and delete if requested
2546 for channel in self.channels:
2547 if channel.sync_to_devices:
2548 allepisodes = [ episode for episode in channel.get_all_episodes() if episode.was_downloaded(and_exists=True) ]
2549 device.update_played_or_delete(channel, allepisodes, gl.config.ipod_delete_played_from_db)
2551 if gl.config.ipod_purge_old_episodes:
2552 device.purge()
2554 sync_all_episodes = not bool(episodes)
2556 if episodes is None:
2557 episodes = self.get_all_episodes()
2559 # make sure we have enough space on the device
2560 total_size = 0
2561 free_space = device.get_free_space()
2562 for episode in episodes:
2563 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
2564 filename = episode.local_filename(create=False)
2565 if filename is not None:
2566 total_size += util.calculate_size(str(filename))
2568 if total_size > free_space:
2569 # can be negative because of the 10 MiB for reserved for the iTunesDB
2570 free_space = max( free_space, 0 )
2571 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
2572 title = _('Not enough space left on device.')
2573 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
2574 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
2575 self.notification(message, title)
2576 device.close()
2577 else:
2578 # start syncing!
2579 gPodderSync(device=device, gPodder=self)
2580 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2581 if self.tray_icon:
2582 self.tray_icon.set_synchronisation_device(device)
2584 # The sync process might have updated the status of episodes,
2585 # therefore persist the database here to avoid losing data
2586 db.commit()
2588 def sync_to_ipod_completed(self, device, successful_sync):
2589 device.unregister( 'post-done', self.sync_to_ipod_completed )
2591 if self.tray_icon:
2592 self.tray_icon.release_synchronisation_device()
2594 if not successful_sync:
2595 title = _('Error closing device')
2596 message = _('There has been an error closing your device.')
2597 self.notification(message, title)
2599 # update model for played state updates after sync
2600 util.idle_add(self.updateComboBox)
2602 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2603 if sync_all_episodes:
2604 device.add_tracks(episodes)
2605 # 'only_sync_not_played' must be used or else all the played
2606 # tracks will be copied then immediately deleted
2607 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2608 self.ipod_delete_played(device)
2609 else:
2610 device.add_tracks(episodes, force_played=True)
2611 device.close()
2612 self.update_selected_episode_list_icons()
2614 def ipod_cleanup_callback(self, device, tracks):
2615 title = _('Delete podcasts from device?')
2616 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?')
2617 if len(tracks) > 0 and self.show_confirmation(message, title):
2618 gPodderSync(device=device, gPodder=self)
2619 Thread(target=self.ipod_cleanup_thread, args=[device, tracks]).start()
2621 def ipod_cleanup_thread(self, device, tracks):
2622 device.remove_tracks(tracks)
2624 if not device.close():
2625 title = _('Error closing device')
2626 message = _('There has been an error closing your device.')
2627 gobject.idle_add(self.show_message, message, title)
2629 def on_cleanup_ipod_activate(self, widget, *args):
2630 columns = (
2631 ('title', None, None, _('Episode')),
2632 ('podcast', None, None, _('Podcast')),
2633 ('filesize', None, None, _('Size')),
2634 ('modified', None, None, _('Copied')),
2635 ('playcount', None, None, _('Play count')),
2636 ('released', None, None, _('Released')),
2639 device = sync.open_device()
2641 if device is None:
2642 title = _('No device configured')
2643 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2644 self.show_message(message, title)
2645 return
2647 if not device.open():
2648 title = _('Cannot open device')
2649 message = _('There has been an error opening your device.')
2650 self.show_message(message, title)
2651 return
2653 tracks = device.get_all_tracks()
2654 if len(tracks) > 0:
2655 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2656 wanted_columns = []
2657 for key, sort_name, sort_type, caption in columns:
2658 want_this_column = False
2659 for track in tracks:
2660 if getattr(track, key) is not None:
2661 want_this_column = True
2662 break
2664 if want_this_column:
2665 wanted_columns.append((key, sort_name, sort_type, caption))
2666 title = _('Remove podcasts from device')
2667 instructions = _('Select the podcast episodes you want to remove from your device.')
2668 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2669 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2670 else:
2671 title = _('No files on device')
2672 message = _('The devices contains no files to be removed.')
2673 self.show_message(message, title)
2674 device.close()
2676 def on_manage_device_playlist(self, widget):
2677 # make sure gpod is available before even trying to sync
2678 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2679 title = _('Cannot manage iPod playlist')
2680 message = _('This feature is not available for iPods.')
2681 self.notification( message, title )
2682 return
2683 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2684 title = _('Cannot manage MTP device playlist')
2685 message = _('This feature is not available for MTP devices.')
2686 self.notification( message, title )
2687 return
2689 device = sync.open_device()
2691 if device is None:
2692 title = _('No device configured')
2693 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2694 self.notification(message, title)
2695 return
2697 if not device.open():
2698 title = _('Cannot open device')
2699 message = _('There has been an error opening your device.')
2700 self.notification(message, title)
2701 return
2703 gPodderPlaylist(device=device, gPodder=self)
2704 device.close()
2706 def show_hide_tray_icon(self):
2707 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2708 self.tray_icon = trayicon.GPodderStatusIcon(self, gpodder.icon_file)
2709 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2710 self.tray_icon.set_visible(False)
2711 del self.tray_icon
2712 self.tray_icon = None
2714 if gl.config.minimize_to_tray and self.tray_icon:
2715 self.tray_icon.set_visible(self.minimized)
2716 elif self.tray_icon:
2717 self.tray_icon.set_visible(True)
2719 def on_itemShowToolbar_activate(self, widget):
2720 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2722 def on_itemShowDescription_activate(self, widget):
2723 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2725 def update_item_device( self):
2726 if gl.config.device_type != 'none':
2727 self.itemDevice.set_visible(True)
2728 self.itemDevice.label = gl.get_device_name()
2729 else:
2730 self.itemDevice.set_visible(False)
2732 def properties_closed( self):
2733 self.show_hide_tray_icon()
2734 self.update_item_device()
2735 self.updateComboBox()
2737 def on_itemPreferences_activate(self, widget, *args):
2738 if gpodder.interface == gpodder.GUI:
2739 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2740 else:
2741 gPodderMaemoPreferences()
2743 def on_itemDependencies_activate(self, widget):
2744 gPodderDependencyManager()
2746 def on_add_new_google_search(self, widget, *args):
2747 def add_google_video_search(query):
2748 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2750 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2752 def on_upgrade_from_videocenter(self, widget):
2753 from gpodder import nokiavideocenter
2754 vc = nokiavideocenter.UpgradeFromVideocenter()
2755 if vc.db2opml():
2756 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))
2757 else:
2758 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2760 def require_my_gpodder_authentication(self):
2761 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2762 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'))
2763 if success and authentication[0] and authentication[1]:
2764 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2765 return True
2766 else:
2767 return False
2769 return True
2771 def my_gpodder_offer_autoupload(self):
2772 if not gl.config.my_gpodder_autoupload:
2773 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')):
2774 gl.config.my_gpodder_autoupload = True
2776 def on_download_from_mygpo(self, widget):
2777 if self.require_my_gpodder_authentication():
2778 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2779 opml_data = client.download_subscriptions()
2780 if len(opml_data) > 0:
2781 fp = open(gl.channel_opml_file, 'w')
2782 fp.write(opml_data)
2783 fp.close()
2784 (added, skipped) = (0, 0)
2785 i = opml.Importer(gl.channel_opml_file)
2786 for item in i.items:
2787 url = item['url']
2788 if url not in (c.url for c in self.channels):
2789 self.add_new_channel(url, ask_download_new=False, block=True)
2790 added += 1
2791 else:
2792 log('Already added: %s', url, sender=self)
2793 skipped += 1
2794 self.updateComboBox()
2795 if added > 0:
2796 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2797 elif widget is not None:
2798 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2799 self.my_gpodder_offer_autoupload()
2800 else:
2801 gl.config.my_gpodder_password = ''
2802 self.on_download_from_mygpo(widget)
2803 else:
2804 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2806 def on_upload_to_mygpo(self, widget):
2807 if self.require_my_gpodder_authentication():
2808 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2809 save_channels(self.channels)
2810 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2811 if widget is not None:
2812 self.show_message('\n'.join(messages), _('Results of upload'))
2813 if not success:
2814 gl.config.my_gpodder_password = ''
2815 self.on_upload_to_mygpo(widget)
2816 else:
2817 self.my_gpodder_offer_autoupload()
2818 elif not success:
2819 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2820 elif widget is not None:
2821 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2823 def on_itemAddChannel_activate(self, widget, *args):
2824 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2826 def on_itemEditChannel_activate(self, widget, *args):
2827 if self.active_channel is None:
2828 title = _('No podcast selected')
2829 message = _('Please select a podcast in the podcasts list to edit.')
2830 self.show_message( message, title)
2831 return
2833 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2835 def change_channel_url(self, channel, new_url):
2836 old_url = channel.url
2837 log('=> change channel url from %s to %s', old_url, new_url)
2838 channel.url = new_url
2839 # remove etag and last_modified to force an update
2840 channel.etag = ''
2841 channel.last_modified = ''
2842 (success, error) = channel.update()
2843 if not success:
2844 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2845 channel.url = old_url
2847 # Remove old episodes which haven't been downloaded.
2848 db.delete_empty_episodes(channel.id);
2850 # Update the OPML file.
2851 save_channels(self.channels)
2853 # update feed cache and select the podcast with the new URL afterwards
2854 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2856 def on_itemRemoveChannel_activate(self, widget, *args):
2857 try:
2858 if gpodder.interface == gpodder.GUI:
2859 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2860 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2861 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2863 title = _('Remove podcast and episodes?')
2864 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2866 dialog.set_title(title)
2867 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2869 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2870 dialog.vbox.pack_start(cb_ask)
2871 cb_ask.show_all()
2872 affirmative = gtk.RESPONSE_YES
2873 elif gpodder.interface == gpodder.MAEMO:
2874 cb_ask = gtk.CheckButton('') # dummy check button
2875 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2876 affirmative = gtk.RESPONSE_OK
2878 result = dialog.run()
2879 dialog.destroy()
2881 if result == affirmative:
2882 # delete downloaded episodes only if checkbox is unchecked
2883 if cb_ask.get_active() == False:
2884 self.active_channel.remove_downloaded()
2885 else:
2886 log('Not removing downloaded episodes', sender=self)
2888 # Clean up downloads and download directories
2889 gl.clean_up_downloads()
2891 # cancel any active downloads from this channel
2892 for episode in self.active_channel.get_all_episodes():
2893 self.download_status_manager.cancel_by_url(episode.url)
2895 # get the URL of the podcast we want to select next
2896 position = self.channels.index(self.active_channel)
2897 if position == len(self.channels)-1:
2898 # this is the last podcast, so select the URL
2899 # of the item before this one (i.e. the "new last")
2900 select_url = self.channels[position-1].url
2901 else:
2902 # there is a podcast after the deleted one, so
2903 # we simply select the one that comes after it
2904 select_url = self.channels[position+1].url
2906 # Remove the channel
2907 self.active_channel.delete()
2908 self.channels.remove(self.active_channel)
2909 self.channel_list_changed = True
2910 save_channels(self.channels)
2912 # Re-load the channels and select the desired new channel
2913 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2914 except:
2915 log('There has been an error removing the channel.', traceback=True, sender=self)
2916 self.update_podcasts_tab()
2918 def get_opml_filter(self):
2919 filter = gtk.FileFilter()
2920 filter.add_pattern('*.opml')
2921 filter.add_pattern('*.xml')
2922 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2923 return filter
2925 def on_item_import_from_file_activate(self, widget, filename=None):
2926 if filename is None:
2927 if gpodder.interface == gpodder.GUI:
2928 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2929 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2930 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2931 elif gpodder.interface == gpodder.MAEMO:
2932 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2933 dlg.set_filter(self.get_opml_filter())
2934 response = dlg.run()
2935 filename = None
2936 if response == gtk.RESPONSE_OK:
2937 filename = dlg.get_filename()
2938 dlg.destroy()
2940 if filename is not None:
2941 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))
2943 def on_itemExportChannels_activate(self, widget, *args):
2944 if not self.channels:
2945 title = _('Nothing to export')
2946 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2947 self.show_message( message, title)
2948 return
2950 if gpodder.interface == gpodder.GUI:
2951 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2952 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2953 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2954 elif gpodder.interface == gpodder.MAEMO:
2955 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2956 dlg.set_filter(self.get_opml_filter())
2957 response = dlg.run()
2958 if response == gtk.RESPONSE_OK:
2959 filename = dlg.get_filename()
2960 dlg.destroy()
2961 exporter = opml.Exporter( filename)
2962 if exporter.write(self.channels):
2963 if len(self.channels) == 1:
2964 title = _('One subscription exported')
2965 else:
2966 title = _('%d subscriptions exported') % len(self.channels)
2967 self.show_message(_('Your podcast list has been successfully exported.'), title)
2968 else:
2969 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2970 else:
2971 dlg.destroy()
2973 def on_itemImportChannels_activate(self, widget, *args):
2974 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))
2976 def on_homepage_activate(self, widget, *args):
2977 util.open_website(gpodder.__url__)
2979 def on_wiki_activate(self, widget, *args):
2980 util.open_website('http://wiki.gpodder.org/')
2982 def on_bug_tracker_activate(self, widget, *args):
2983 util.open_website('http://bugs.gpodder.org/')
2985 def on_itemAbout_activate(self, widget, *args):
2986 dlg = gtk.AboutDialog()
2987 dlg.set_name('gPodder')
2988 dlg.set_version(gpodder.__version__)
2989 dlg.set_copyright(gpodder.__copyright__)
2990 dlg.set_website(gpodder.__url__)
2991 dlg.set_translator_credits( _('translator-credits'))
2992 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2994 if gpodder.interface == gpodder.GUI:
2995 # For the "GUI" version, we add some more
2996 # items to the about dialog (credits and logo)
2997 dlg.set_authors(app_authors)
2998 try:
2999 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3000 except:
3001 dlg.set_logo_icon_name('gpodder')
3003 dlg.run()
3005 def on_wNotebook_switch_page(self, widget, *args):
3006 page_num = args[1]
3007 if gpodder.interface == gpodder.MAEMO:
3008 page = self.wNotebook.get_nth_page(page_num)
3009 tab_label = self.wNotebook.get_tab_label(page).get_text()
3010 if page_num == 0 and self.active_channel is not None:
3011 self.set_title(self.active_channel.title)
3012 else:
3013 self.set_title(tab_label)
3014 if page_num == 0:
3015 self.play_or_download()
3016 self.menuChannels.set_sensitive(True)
3017 self.menuSubscriptions.set_sensitive(True)
3018 # The message area in the downloads tab should be hidden
3019 # when the user switches away from the downloads tab
3020 if self.message_area is not None:
3021 self.message_area.hide()
3022 self.message_area = None
3023 else:
3024 self.menuChannels.set_sensitive(False)
3025 self.menuSubscriptions.set_sensitive(False)
3026 self.toolDownload.set_sensitive( False)
3027 self.toolPlay.set_sensitive( False)
3028 self.toolTransfer.set_sensitive( False)
3029 self.toolCancel.set_sensitive( False)#services.download_status_manager.has_items())
3031 def on_treeChannels_row_activated(self, widget, path, *args):
3032 # double-click action of the podcast list or enter
3033 self.treeChannels.set_cursor(path)
3035 def on_treeChannels_cursor_changed(self, widget, *args):
3036 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3038 if model is not None and iter is not None:
3039 old_active_channel = self.active_channel
3040 (id,) = model.get_path(iter)
3041 self.active_channel = self.channels[id]
3043 if self.active_channel == old_active_channel:
3044 return
3046 if gpodder.interface == gpodder.MAEMO:
3047 self.set_title(self.active_channel.title)
3048 self.itemEditChannel.set_visible(True)
3049 self.itemRemoveChannel.set_visible(True)
3050 self.channel_toggle_lock.set_visible(True)
3051 if self.active_channel.channel_is_locked:
3052 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
3053 else:
3054 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
3056 else:
3057 self.active_channel = None
3058 self.itemEditChannel.set_visible(False)
3059 self.itemRemoveChannel.set_visible(False)
3060 self.channel_toggle_lock.set_visible(False)
3062 self.updateTreeView()
3064 def on_entryAddChannel_changed(self, widget, *args):
3065 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
3066 self.btnAddChannel.set_sensitive( active)
3068 def on_btnAddChannel_clicked(self, widget, *args):
3069 url = self.entryAddChannel.get_text()
3070 self.entryAddChannel.set_text('')
3071 self.add_new_channel( url)
3073 def on_btnEditChannel_clicked(self, widget, *args):
3074 self.on_itemEditChannel_activate( widget, args)
3076 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
3078 What this function does depends on from which widget it is called.
3079 It gets the selected episodes of the current podcast and runs one
3080 of the following actions on them:
3082 * Transfer (to MP3 player, iPod, etc..)
3083 * Playback/open files
3084 * Show the episode info dialog
3085 * Download episodes
3087 try:
3088 selection = self.treeAvailable.get_selection()
3089 (model, paths) = selection.get_selected_rows()
3091 if len(paths) == 0:
3092 log('Nothing selected', sender=self)
3093 return
3095 wname = widget.get_name()
3096 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
3097 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
3098 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
3100 episodes = []
3101 for path in paths:
3102 it = model.get_iter(path)
3103 url = model.get_value(it, 0)
3104 episode = self.active_channel.find_episode(url)
3105 episodes.append(episode)
3107 if len(episodes) == 0:
3108 log('No episodes selected', sender=self)
3110 if do_transfer:
3111 self.on_sync_to_ipod_activate(widget, episodes)
3112 elif do_playback:
3113 for episode in episodes:
3114 if episode.was_downloaded(and_exists=True):
3115 self.playback_episode(episode)
3116 elif gl.config.enable_streaming:
3117 self.playback_episode(episode, stream=True)
3118 elif do_epdialog:
3119 play_callback = lambda: self.playback_episode(episode)
3120 def download_callback():
3121 self.download_episode_list([episode])
3122 self.play_or_download()
3123 if self.gpodder_episode_window is None:
3124 log('First-time use of episode window --- creating', sender=self)
3125 self.gpodder_episode_window = gPodderEpisode(\
3126 download_status_manager=self.download_status_manager, \
3127 episode_is_downloading=self.episode_is_downloading)
3128 self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
3129 else:
3130 self.download_episode_list(episodes)
3131 self.update_selected_episode_list_icons()
3132 self.play_or_download()
3133 except:
3134 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
3136 def on_treeAvailable_button_release_event(self, widget, *args):
3137 self.play_or_download()
3139 def auto_update_procedure(self, first_run=False):
3140 log('auto_update_procedure() got called', sender=self)
3141 if not first_run and gl.config.auto_update_feeds and self.minimized:
3142 self.update_feed_cache(force_update=True)
3144 next_update = 60*1000*gl.config.auto_update_frequency
3145 gobject.timeout_add(next_update, self.auto_update_procedure)
3147 def on_treeDownloads_row_activated(self, widget, *args):
3148 if self.wNotebook.get_current_page() == 0:
3149 # Use the available podcasts treeview + model
3150 selection = self.treeAvailable.get_selection()
3151 (model, paths) = selection.get_selected_rows()
3152 urls = [model.get_value(model.get_iter(path), 0) for path in paths]
3153 selected_tasks = [task for task in self.download_tasks_seen if task.url in urls]
3154 for task in selected_tasks:
3155 task.status = task.CANCELLED
3156 self.update_selected_episode_list_icons()
3157 self.play_or_download()
3158 return
3160 # Use the standard way of working on the treeview
3161 selection = self.treeDownloads.get_selection()
3162 (model, paths) = selection.get_selected_rows()
3163 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3165 for tree_row_reference, task in selected_tasks:
3166 if task.status in (task.DOWNLOADING, task.QUEUED):
3167 task.status = task.PAUSED
3168 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3169 self.download_queue_manager.add_task(task)
3170 elif task.status == task.DONE:
3171 model.remove(model.get_iter(tree_row_reference.get_path()))
3173 self.play_or_download()
3175 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
3176 self.on_treeDownloads_row_activated( widget, None)
3178 def on_btnCancelAll_clicked(self, widget, *args):
3179 self.treeDownloads.get_selection().select_all()
3180 self.on_treeDownloads_row_activated( self.toolCancel, None)
3181 self.treeDownloads.get_selection().unselect_all()
3183 def on_btnDownloadedDelete_clicked(self, widget, *args):
3184 if self.active_channel is None:
3185 return
3187 channel_url = self.active_channel.url
3188 selection = self.treeAvailable.get_selection()
3189 ( model, paths ) = selection.get_selected_rows()
3191 if selection.count_selected_rows() == 0:
3192 log( 'Nothing selected - will not remove any downloaded episode.')
3193 return
3195 if selection.count_selected_rows() == 1:
3196 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
3198 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
3199 if episode['is_locked']:
3200 title = _('%s is locked') % episode_title
3201 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
3202 self.notification(message, title)
3203 return
3205 title = _('Remove %s?') % episode_title
3206 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.")
3207 else:
3208 title = _('Remove %d episodes?') % selection.count_selected_rows()
3209 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.')
3211 locked_count = 0
3212 for path in paths:
3213 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
3214 if episode['is_locked']:
3215 locked_count += 1
3217 if selection.count_selected_rows() == locked_count:
3218 title = _('Episodes are locked')
3219 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3220 self.notification(message, title)
3221 return
3222 elif locked_count > 0:
3223 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
3224 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.')
3226 # if user confirms deletion, let's remove some stuff ;)
3227 if self.show_confirmation( message, title):
3228 try:
3229 # iterate over the selection, see also on_treeDownloads_row_activated
3230 for path in paths:
3231 url = model.get_value( model.get_iter( path), 0)
3232 self.active_channel.delete_episode_by_url( url)
3234 # now, clear local db cache so we can re-read it
3235 self.updateComboBox()
3236 except:
3237 log( 'Error while deleting (some) downloads.', traceback=True, sender=self)
3239 # only delete partial files if we do not have any downloads in progress
3240 delete_partial = False #not services.download_status_manager.has_items()
3241 gl.clean_up_downloads(delete_partial)
3242 self.update_selected_episode_list_icons()
3243 self.play_or_download()
3245 def on_key_press(self, widget, event):
3246 # Allow tab switching with Ctrl + PgUp/PgDown
3247 if event.state & gtk.gdk.CONTROL_MASK:
3248 if event.keyval == gtk.keysyms.Page_Up:
3249 self.wNotebook.prev_page()
3250 return True
3251 elif event.keyval == gtk.keysyms.Page_Down:
3252 self.wNotebook.next_page()
3253 return True
3255 # After this code we only handle Maemo hardware keys,
3256 # so if we are not a Maemo app, we don't do anything
3257 if gpodder.interface != gpodder.MAEMO:
3258 return False
3260 if event.keyval == gtk.keysyms.F6:
3261 if self.fullscreen:
3262 self.window.unfullscreen()
3263 else:
3264 self.window.fullscreen()
3265 if event.keyval == gtk.keysyms.Escape:
3266 new_visibility = not self.vboxChannelNavigator.get_property('visible')
3267 self.vboxChannelNavigator.set_property('visible', new_visibility)
3268 self.column_size.set_visible(not new_visibility)
3269 self.column_released.set_visible(not new_visibility)
3271 diff = 0
3272 if event.keyval == gtk.keysyms.F7: #plus
3273 diff = 1
3274 elif event.keyval == gtk.keysyms.F8: #minus
3275 diff = -1
3277 if diff != 0 and not self.currently_updating:
3278 selection = self.treeChannels.get_selection()
3279 (model, iter) = selection.get_selected()
3280 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3281 selection.select_path(new_path)
3282 self.treeChannels.set_cursor(new_path)
3283 return True
3285 return False
3287 def window_state_event(self, widget, event):
3288 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
3289 self.fullscreen = True
3290 else:
3291 self.fullscreen = False
3293 old_minimized = self.minimized
3295 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
3296 if gpodder.interface == gpodder.MAEMO:
3297 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
3299 if old_minimized != self.minimized and self.tray_icon:
3300 self.gPodder.set_skip_taskbar_hint(self.minimized)
3301 elif not self.tray_icon:
3302 self.gPodder.set_skip_taskbar_hint(False)
3304 if gl.config.minimize_to_tray and self.tray_icon:
3305 self.tray_icon.set_visible(self.minimized)
3307 def uniconify_main_window(self):
3308 if self.minimized:
3309 self.gPodder.present()
3311 def iconify_main_window(self):
3312 if not self.minimized:
3313 self.gPodder.iconify()
3315 def update_podcasts_tab(self):
3316 if len(self.channels):
3317 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3318 else:
3319 self.label2.set_text(_('Podcasts'))
3321 @dbus.service.method(gpodder.dbus_interface)
3322 def show_gui_window(self):
3323 self.gPodder.present()
3325 class gPodderChannel(BuilderWidget):
3326 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
3328 def new(self):
3329 global WEB_BROWSER_ICON
3330 self.changed = False
3331 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
3332 self.gPodderChannel.set_title( self.channel.title)
3333 self.entryTitle.set_text( self.channel.title)
3334 self.entryURL.set_text( self.channel.url)
3336 self.LabelDownloadTo.set_text( self.channel.save_dir)
3337 self.LabelWebsite.set_text( self.channel.link)
3339 self.cbNoSync.set_active( not self.channel.sync_to_devices)
3340 self.musicPlaylist.set_text(self.channel.device_playlist_name)
3341 if self.channel.username:
3342 self.FeedUsername.set_text( self.channel.username)
3343 if self.channel.password:
3344 self.FeedPassword.set_text( self.channel.password)
3346 services.cover_downloader.register('cover-available', self.cover_download_finished)
3347 services.cover_downloader.request_cover(self.channel)
3349 # Hide the website button if we don't have a valid URL
3350 if not self.channel.link:
3351 self.btn_website.hide_all()
3353 b = gtk.TextBuffer()
3354 b.set_text( self.channel.description)
3355 self.channel_description.set_buffer( b)
3357 #Add Drag and Drop Support
3358 flags = gtk.DEST_DEFAULT_ALL
3359 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
3360 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
3361 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
3362 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
3364 def on_btn_website_clicked(self, widget):
3365 util.open_website(self.channel.link)
3367 def on_btnDownloadCover_clicked(self, widget):
3368 if gpodder.interface == gpodder.GUI:
3369 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3370 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3371 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3372 elif gpodder.interface == gpodder.MAEMO:
3373 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
3375 if dlg.run() == gtk.RESPONSE_OK:
3376 url = dlg.get_uri()
3377 services.cover_downloader.replace_cover(self.channel, url)
3379 dlg.destroy()
3381 def on_btnClearCover_clicked(self, widget):
3382 services.cover_downloader.replace_cover(self.channel)
3384 def cover_download_finished(self, channel_url, pixbuf):
3385 if pixbuf is not None:
3386 self.imgCover.set_from_pixbuf(pixbuf)
3387 self.gPodderChannel.show()
3389 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
3390 files = sel.data.strip().split('\n')
3391 if len(files) != 1:
3392 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
3393 return
3395 file = files[0]
3397 if file.startswith('file://') or file.startswith('http://'):
3398 services.cover_downloader.replace_cover(self.channel, file)
3399 return
3401 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
3403 def on_gPodderChannel_destroy(self, widget, *args):
3404 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
3406 def on_btnOK_clicked(self, widget, *args):
3407 entered_url = self.entryURL.get_text()
3408 channel_url = self.channel.url
3410 if entered_url != channel_url:
3411 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
3412 if hasattr(self, 'callback_change_url'):
3413 self.gPodderChannel.hide_all()
3414 self.callback_change_url(self.channel, entered_url)
3416 self.channel.sync_to_devices = not self.cbNoSync.get_active()
3417 self.channel.device_playlist_name = self.musicPlaylist.get_text()
3418 self.channel.set_custom_title( self.entryTitle.get_text())
3419 self.channel.username = self.FeedUsername.get_text().strip()
3420 self.channel.password = self.FeedPassword.get_text()
3421 self.channel.save()
3423 self.gPodderChannel.destroy()
3424 self.callback_closed()
3426 class gPodderAddPodcastDialog(BuilderWidget):
3427 finger_friendly_widgets = ['btn_close', 'btn_add']
3429 def new(self):
3430 if not hasattr(self, 'url_callback'):
3431 log('No url callback set', sender=self)
3432 self.url_callback = None
3433 if hasattr(self, 'custom_label'):
3434 self.label_add.set_text(self.custom_label)
3435 if hasattr(self, 'custom_title'):
3436 self.gPodderAddPodcastDialog.set_title(self.custom_title)
3437 if gpodder.interface == gpodder.MAEMO:
3438 self.entry_url.set_text('http://')
3439 self.gPodderAddPodcastDialog.show()
3441 def on_btn_close_clicked(self, widget):
3442 self.gPodderAddPodcastDialog.destroy()
3444 def on_btn_paste_clicked(self, widget):
3445 clipboard = gtk.Clipboard()
3446 clipboard.request_text(self.receive_clipboard_text)
3448 def receive_clipboard_text(self, clipboard, text, data=None):
3449 if text is not None:
3450 self.entry_url.set_text(text)
3451 else:
3452 self.show_message(_('Nothing to paste.'), _('Clipboard is empty'))
3454 def on_entry_url_changed(self, widget):
3455 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
3457 def on_btn_add_clicked(self, widget):
3458 url = self.entry_url.get_text()
3459 self.on_btn_close_clicked(widget)
3460 if self.url_callback is not None:
3461 self.url_callback(url)
3464 class gPodderMaemoPreferences(BuilderWidget):
3465 finger_friendly_widgets = ['btn_close', 'btn_advanced']
3466 audio_players = (
3467 ('default', 'Nokia Media Player'),
3468 ('panucci', 'Panucci'),
3470 video_players = (
3471 ('default', 'Nokia Media Player'),
3472 ('mplayer', 'MPlayer'),
3475 def new(self):
3476 gl.config.connect_gtk_togglebutton('display_tray_icon', self.check_show_status_icon)
3477 gl.config.connect_gtk_togglebutton('on_quit_ask', self.check_ask_on_quit)
3479 # Set up the audio player combobox
3480 found = False
3481 self.userconfigured_player = None
3482 for id, audio_player in enumerate(self.audio_players):
3483 command, caption = audio_player
3484 self.combo_player_model.append([caption])
3485 if gl.config.player == command:
3486 self.combo_player.set_active(id)
3487 found = True
3488 if not found:
3489 self.combo_player_model.append(['User-configured (%s)' % gl.config.player])
3490 self.combo_player.set_active(len(self.combo_player_model)-1)
3491 self.userconfigured_player = gl.config.player
3493 # Set up the video player combobox
3494 found = False
3495 self.userconfigured_videoplayer = None
3496 for id, video_player in enumerate(self.video_players):
3497 command, caption = video_player
3498 self.combo_videoplayer_model.append([caption])
3499 if gl.config.videoplayer == command:
3500 self.combo_videoplayer.set_active(id)
3501 found = True
3502 if not found:
3503 self.combo_videoplayer_model.append(['User-configured (%s)' % gl.config.videoplayer])
3504 self.combo_videoplayer.set_active(len(self.combo_videoplayer_model)-1)
3505 self.userconfigured_videoplayer = gl.config.videoplayer
3507 def on_combo_player_changed(self, combobox):
3508 index = combobox.get_active()
3509 if index < len(self.audio_players):
3510 gl.config.player = self.audio_players[index][0]
3511 elif self.userconfigured_player is not None:
3512 gl.config.player = self.userconfigured_player
3514 def on_combo_videoplayer_changed(self, combobox):
3515 index = combobox.get_active()
3516 if index < len(self.video_players):
3517 gl.config.videoplayer = self.video_players[index][0]
3518 elif self.userconfigured_videoplayer is not None:
3519 gl.config.videoplayer = self.userconfigured_videoplayer
3521 def on_btn_advanced_clicked(self, widget):
3522 self.gPodderMaemoPreferences.destroy()
3523 gPodderConfigEditor()
3525 def on_btn_close_clicked(self, widget):
3526 self.gPodderMaemoPreferences.destroy()
3529 class gPodderProperties(BuilderWidget):
3530 def new(self):
3531 if not hasattr( self, 'callback_finished'):
3532 self.callback_finished = None
3534 if gpodder.interface == gpodder.MAEMO:
3535 self.table5.hide_all() # player
3536 self.gPodderProperties.fullscreen()
3538 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
3539 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
3540 gl.config.connect_gtk_editable( 'player', self.openApp)
3541 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
3542 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
3543 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
3544 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
3545 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
3546 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
3547 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
3548 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
3549 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
3550 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
3551 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
3552 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
3553 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
3554 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
3555 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
3556 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
3557 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
3558 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
3559 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
3560 gl.config.connect_gtk_togglebutton('ipod_delete_played_from_db', self.ipod_delete_played_from_db)
3561 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
3562 gl.config.connect_gtk_togglebutton('disable_pre_sync_conversion', self.player_supports_ogg)
3564 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
3565 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
3567 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
3569 self.iPodMountpoint.set_label( gl.config.ipod_mount)
3570 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
3571 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
3573 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
3574 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
3576 if tagging_supported():
3577 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
3578 else:
3579 self.updatetags.set_sensitive( False)
3580 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
3581 self.updatetags.set_label( new_label)
3583 # device type
3584 self.comboboxDeviceType.set_active( 0)
3585 if gl.config.device_type == 'ipod':
3586 self.comboboxDeviceType.set_active( 1)
3587 elif gl.config.device_type == 'filesystem':
3588 self.comboboxDeviceType.set_active( 2)
3589 elif gl.config.device_type == 'mtp':
3590 self.comboboxDeviceType.set_active( 3)
3592 # setup cell renderers
3593 cellrenderer = gtk.CellRendererPixbuf()
3594 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
3595 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3596 cellrenderer = gtk.CellRendererText()
3597 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
3598 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3600 cellrenderer = gtk.CellRendererPixbuf()
3601 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
3602 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3603 cellrenderer = gtk.CellRendererText()
3604 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
3605 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3607 if not hasattr(self, 'user_apps_reader'):
3608 self.user_apps_reader = UserAppsReader(['audio', 'video'])
3610 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
3611 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
3613 if gpodder.interface == gpodder.GUI:
3614 self.user_apps_reader.read()
3616 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
3617 index = self.find_active_audio_app()
3618 self.comboAudioPlayerApp.set_active(index)
3619 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
3620 index = self.find_active_video_app()
3621 self.comboVideoPlayerApp.set_active(index)
3623 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
3625 def is_row_separator(self, model, iter):
3626 return model.get_value(iter, 0) == ''
3628 def update_mountpoint( self, ipod):
3629 if ipod is None or ipod.mount_point is None:
3630 self.iPodMountpoint.set_label( '')
3631 else:
3632 self.iPodMountpoint.set_label( ipod.mount_point)
3634 def find_active_audio_app(self):
3635 index_custom = -1
3636 model = self.comboAudioPlayerApp.get_model()
3637 iter = model.get_iter_first()
3638 index = 0
3639 while iter is not None:
3640 command = model.get_value(iter, 1)
3641 if command == self.openApp.get_text():
3642 return index
3643 if index_custom < 0 and command == '':
3644 index_custom = index
3645 iter = model.iter_next(iter)
3646 index += 1
3647 # return index of custom command or first item
3648 return max(0, index_custom)
3650 def find_active_video_app( self):
3651 index_custom = -1
3652 model = self.comboVideoPlayerApp.get_model()
3653 iter = model.get_iter_first()
3654 index = 0
3655 while iter is not None:
3656 command = model.get_value(iter, 1)
3657 if command == self.openVideoApp.get_text():
3658 return index
3659 if index_custom < 0 and command == '':
3660 index_custom = index
3661 iter = model.iter_next(iter)
3662 index += 1
3663 # return index of custom command or first item
3664 return max(0, index_custom)
3666 def set_download_dir( self, new_download_dir, event = None):
3667 gl.downloaddir = self.chooserDownloadTo.get_filename()
3668 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3669 self.notification(_('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
3671 if event:
3672 event.set()
3674 def on_auto_update_feeds_toggled( self, widget, *args):
3675 self.auto_update_frequency.set_sensitive(widget.get_active())
3677 def on_display_tray_icon_toggled( self, widget, *args):
3678 self.enable_notifications.set_sensitive(widget.get_active())
3679 self.minimize_to_tray.set_sensitive(widget.get_active())
3681 def on_cbCustomSyncName_toggled( self, widget, *args):
3682 self.entryCustomSyncName.set_sensitive( widget.get_active())
3684 def on_only_sync_not_played_toggled( self, widget, *args):
3685 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3686 if not widget.get_active():
3687 self.delete_episodes_marked_played.set_active(False)
3689 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3690 if widget.get_active() and self.only_sync_not_played.get_active():
3691 self.on_sync_leave.set_active(True)
3692 self.on_sync_delete.set_sensitive(not widget.get_active())
3693 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3695 def on_btnCustomSyncNameHelp_clicked( self, widget):
3696 examples = [
3697 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3698 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3699 '<i>{episode.published}</i> -&gt; <b>20070908</b> (for 08.09.2007)',
3700 '<i>{episode.pubtime}</i> -&gt; <b>1344</b> (for 13:44)',
3701 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3704 info = [
3705 _('You can specify a custom format string for the file names on your MP3 player here.'),
3706 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3707 '\n'.join( [ ' %s' % s for s in examples ])
3710 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3712 def on_gPodderProperties_destroy(self, widget, *args):
3713 self.on_btnOK_clicked( widget, *args)
3715 def on_btnConfigEditor_clicked(self, widget, *args):
3716 self.on_btnOK_clicked(widget, *args)
3717 gPodderConfigEditor()
3719 def on_comboAudioPlayerApp_changed(self, widget, *args):
3720 # find out which one
3721 iter = self.comboAudioPlayerApp.get_active_iter()
3722 model = self.comboAudioPlayerApp.get_model()
3723 command = model.get_value( iter, 1)
3724 if command == '':
3725 if self.openApp.get_text() == 'default':
3726 self.openApp.set_text('')
3727 self.openApp.set_sensitive( True)
3728 self.openApp.show()
3729 self.labelCustomCommand.show()
3730 else:
3731 self.openApp.set_text( command)
3732 self.openApp.set_sensitive( False)
3733 self.openApp.hide()
3734 self.labelCustomCommand.hide()
3736 def on_comboVideoPlayerApp_changed(self, widget, *args):
3737 # find out which one
3738 iter = self.comboVideoPlayerApp.get_active_iter()
3739 model = self.comboVideoPlayerApp.get_model()
3740 command = model.get_value(iter, 1)
3741 if command == '':
3742 if self.openVideoApp.get_text() == 'default':
3743 self.openVideoApp.set_text('')
3744 self.openVideoApp.set_sensitive(True)
3745 self.openVideoApp.show()
3746 self.labelCustomVideoCommand.show()
3747 else:
3748 self.openVideoApp.set_text(command)
3749 self.openVideoApp.set_sensitive(False)
3750 self.openVideoApp.hide()
3751 self.labelCustomVideoCommand.hide()
3753 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3754 sens = not self.cbEnvironmentVariables.get_active()
3755 self.httpProxy.set_sensitive( sens)
3756 self.ftpProxy.set_sensitive( sens)
3758 def on_comboboxDeviceType_changed(self, widget, *args):
3759 active_item = self.comboboxDeviceType.get_active()
3761 # None
3762 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3763 self.imageSyncOptions, self. separatorSyncOptions,
3764 self.on_sync_mark_played, self.on_sync_delete,
3765 self.on_sync_leave, self.label_after_sync,
3766 self.delete_episodes_marked_played,
3767 self.player_supports_ogg )
3769 for widget in sync_widgets:
3770 if active_item == 0:
3771 widget.hide_all()
3772 else:
3773 widget.show_all()
3775 # iPod
3776 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3777 self.ipod_write_gtkpod_extended,
3778 self.ipod_delete_played_from_db)
3780 for widget in ipod_widgets:
3781 if active_item == 1:
3782 widget.show_all()
3783 else:
3784 widget.hide_all()
3786 # filesystem-based MP3 player
3787 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3788 self.cbChannelSubfolder, self.cbCustomSyncName,
3789 self.entryCustomSyncName, self.btnCustomSyncNameHelp,
3790 self.player_supports_ogg )
3792 for widget in fs_widgets:
3793 if active_item == 2:
3794 widget.show_all()
3795 else:
3796 widget.hide_all()
3798 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3799 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3800 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3801 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3802 fs.set_current_folder(self.iPodMountpoint.get_label())
3803 if fs.run() == gtk.RESPONSE_OK:
3804 self.iPodMountpoint.set_label( fs.get_filename())
3805 fs.destroy()
3807 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3808 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3809 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3810 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3811 fs.set_current_folder(self.filesystemMountpoint.get_label())
3812 if fs.run() == gtk.RESPONSE_OK:
3813 self.filesystemMountpoint.set_label( fs.get_filename())
3814 fs.destroy()
3816 def on_btnOK_clicked(self, widget, *args):
3817 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3818 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3820 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3821 new_download_dir = self.chooserDownloadTo.get_filename()
3822 download_dir_size = util.calculate_size( gl.downloaddir)
3823 download_dir_size_string = gl.format_filesize( download_dir_size)
3824 event = Event()
3826 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3827 dlg.vbox.set_spacing( 5)
3828 dlg.set_border_width( 5)
3830 label = gtk.Label()
3831 label.set_line_wrap( True)
3832 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3833 myprogressbar = gtk.ProgressBar()
3835 # put it all together
3836 dlg.vbox.pack_start( label)
3837 dlg.vbox.pack_end( myprogressbar)
3839 # switch windows
3840 dlg.show_all()
3841 self.gPodderProperties.hide_all()
3843 # hide action area and separator line
3844 dlg.action_area.hide()
3845 dlg.set_has_separator( False)
3847 args = ( new_download_dir, event, )
3849 thread = Thread( target = self.set_download_dir, args = args)
3850 thread.start()
3852 while not event.isSet():
3853 try:
3854 new_download_dir_size = util.calculate_size( new_download_dir)
3855 except:
3856 new_download_dir_size = 0
3857 if download_dir_size > 0:
3858 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3859 else:
3860 fract = 0.0
3861 if fract < 0.99:
3862 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3863 else:
3864 myprogressbar.set_text( _('Finishing... please wait.'))
3865 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3866 event.wait( 0.1)
3867 while gtk.events_pending():
3868 gtk.main_iteration( False)
3870 dlg.destroy()
3872 device_type = self.comboboxDeviceType.get_active()
3873 if device_type == 0:
3874 gl.config.device_type = 'none'
3875 elif device_type == 1:
3876 gl.config.device_type = 'ipod'
3877 elif device_type == 2:
3878 gl.config.device_type = 'filesystem'
3879 elif device_type == 3:
3880 gl.config.device_type = 'mtp'
3881 self.gPodderProperties.destroy()
3882 if self.callback_finished:
3883 self.callback_finished()
3886 class gPodderEpisode(BuilderWidget):
3887 finger_friendly_widgets = ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3889 def new(self):
3890 setattr(self, 'episode', None)
3891 setattr(self, 'download_callback', None)
3892 setattr(self, 'play_callback', None)
3893 self.gPodderEpisode.connect('delete-event', self.on_delete_event)
3894 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3895 self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
3896 if gl.config.enable_html_shownotes:
3897 try:
3898 import gtkhtml2
3899 setattr(self, 'have_gtkhtml2', True)
3900 # Generate a HTML view and remove the textview
3901 setattr(self, 'htmlview', gtkhtml2.View())
3902 self.scrolled_window.remove(self.scrolled_window.get_child())
3903 self.scrolled_window.add(self.htmlview)
3904 self.textview = None
3905 self.htmlview.set_document(gtkhtml2.Document())
3906 self.htmlview.show()
3907 except ImportError:
3908 log('Install gtkhtml2 if you want HTML shownotes', sender=self)
3909 setattr(self, 'have_gtkhtml2', False)
3910 else:
3911 setattr(self, 'have_gtkhtml2', False)
3912 self.gPodderEpisode.connect('key-press-event', self.on_key_press)
3914 def on_key_press(self, widget, event):
3915 vsb = self.scrolled_window.get_vscrollbar()
3916 vadj = vsb.get_adjustment()
3917 step = vadj.step_increment
3918 if event.keyval in (gtk.keysyms.J, gtk.keysyms.j):
3919 vsb.set_value(vsb.get_value() + step)
3920 elif event.keyval in (gtk.keysyms.K, gtk.keysyms.k):
3921 vsb.set_value(vsb.get_value() - step)
3923 def show(self, episode, download_callback, play_callback):
3924 self.download_progress.set_fraction(0)
3925 self.download_progress.set_text(_('Please wait...'))
3926 self.episode = episode
3927 self.download_callback = download_callback
3928 self.play_callback = play_callback
3930 self.gPodderEpisode.set_title(self.episode.title)
3932 if self.have_gtkhtml2:
3933 import gtkhtml2
3934 d = gtkhtml2.Document()
3935 d.open_stream('text/html')
3936 d.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3937 d.close_stream()
3938 self.htmlview.set_document(d)
3939 else:
3940 b = gtk.TextBuffer()
3941 self.textview.set_buffer(b)
3943 self.hide_show_widgets()
3944 self.gPodderEpisode.show()
3946 # Make sure the window comes up right now:
3947 while gtk.events_pending():
3948 gtk.main_iteration(False)
3950 # Now do the stuff that takes a bit longer...
3951 heading = self.episode.title
3952 subheading = 'from %s' % (self.episode.channel.title)
3953 description = self.episode.description
3954 footer = []
3956 if self.have_gtkhtml2:
3957 import gtkhtml2
3958 d.connect('link-clicked', lambda d, url: util.open_website(url))
3959 def request_url(document, url, stream):
3960 def opendata(url, stream):
3961 fp = urllib2.urlopen(url)
3962 data = fp.read(1024*10)
3963 while data != '':
3964 stream.write(data)
3965 data = fp.read(1024*10)
3966 stream.close()
3967 Thread(target=opendata, args=[url, stream]).start()
3968 d.connect('request-url', request_url)
3969 d.clear()
3970 d.open_stream('text/html')
3971 d.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3972 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)))
3973 d.write_stream(self.episode.description)
3974 if len(footer):
3975 d.write_stream('<hr style="border: 1px #eeeeee solid;">')
3976 d.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils.escape(f) for f in footer))),))
3977 d.write_stream('</p></body></html>')
3978 d.close_stream()
3979 else:
3980 b.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
3981 b.create_tag('subheading', scale=pango.SCALE_SMALL)
3982 b.create_tag('footer', scale=pango.SCALE_SMALL)
3984 b.insert_with_tags_by_name(b.get_end_iter(), heading, 'heading')
3985 b.insert_at_cursor('\n')
3986 b.insert_with_tags_by_name(b.get_end_iter(), subheading, 'subheading')
3987 b.insert_at_cursor('\n\n')
3988 b.insert(b.get_end_iter(), util.remove_html_tags(description))
3989 if len(footer):
3990 b.insert_at_cursor('\n\n')
3991 b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
3992 b.place_cursor(b.get_start_iter())
3994 def on_cancel(self, widget):
3995 self.download_status_manager.cancel_by_url(self.episode.url)
3997 def on_delete_event(self, widget, event):
3998 # Avoid destroying the dialog, simply hide
3999 self.on_close(widget)
4000 return True
4002 def on_close(self, widget):
4003 self.episode = None
4004 if self.have_gtkhtml2:
4005 import gtkhtml2
4006 self.htmlview.set_document(gtkhtml2.Document())
4007 else:
4008 self.textview.get_buffer().set_text('')
4009 self.gPodderEpisode.hide()
4011 def download_status_changed(self, episode_urls):
4012 # Reload the episode from the database, so a newly-set local_filename
4013 # as a result of a download gets updated in the episode object
4014 self.episode.reload_from_db()
4015 self.hide_show_widgets()
4017 def download_status_progress(self, progress, speed):
4018 # We receive this from the main window every time the progress
4019 # for our episode has changed (but only when this window is visible)
4020 self.download_progress.set_fraction(progress)
4021 self.download_progress.set_text('Downloading: %d%% (%s/s)' % (100.*progress, gl.format_filesize(speed)))
4023 def hide_show_widgets(self):
4024 is_downloading = self.episode_is_downloading(self.episode)
4025 if is_downloading:
4026 self.download_progress.show_all()
4027 self.btnCancel.show_all()
4028 self.btnPlay.hide_all()
4029 self.btnDownload.hide_all()
4030 else:
4031 self.download_progress.hide_all()
4032 self.btnCancel.hide_all()
4033 if self.episode.was_downloaded(and_exists=True):
4034 if self.episode.file_type() in ('audio', 'video'):
4035 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
4036 else:
4037 self.btnPlay.set_label(gtk.STOCK_OPEN)
4038 self.btnPlay.set_use_stock(True)
4039 self.btnPlay.show_all()
4040 self.btnDownload.hide_all()
4041 else:
4042 self.btnPlay.hide_all()
4043 self.btnDownload.show_all()
4045 def on_download(self, widget):
4046 if self.download_callback:
4047 self.download_callback()
4049 def on_playback(self, widget):
4050 if self.play_callback:
4051 self.play_callback()
4052 self.on_close(widget)
4054 class gPodderSync(BuilderWidget):
4055 def new(self):
4056 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
4058 self.device.register('progress', self.on_progress)
4059 self.device.register('sub-progress', self.on_sub_progress)
4060 self.device.register('status', self.on_status)
4061 self.device.register('done', self.on_done)
4063 def on_progress(self, pos, max, text=None):
4064 if text is None:
4065 text = _('%d of %d done') % (pos, max)
4066 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
4067 util.idle_add(self.progressbar.set_text, text)
4069 def on_sub_progress(self, percentage):
4070 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
4072 def on_status(self, status):
4073 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
4075 def on_done(self):
4076 util.idle_add(self.gPodderSync.destroy)
4077 if not self.gPodder.minimized:
4078 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
4080 def on_gPodderSync_destroy(self, widget, *args):
4081 self.device.unregister('progress', self.on_progress)
4082 self.device.unregister('sub-progress', self.on_sub_progress)
4083 self.device.unregister('status', self.on_status)
4084 self.device.unregister('done', self.on_done)
4085 self.device.cancel()
4087 def on_cancel_button_clicked(self, widget, *args):
4088 self.device.cancel()
4091 class gPodderOpmlLister(BuilderWidget):
4092 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
4093 (MODE_DOWNLOAD, MODE_SEARCH) = range(2)
4095 def new(self):
4096 # initiate channels list
4097 self.channels = []
4098 self.callback_for_channel = None
4099 self.callback_finished = None
4101 if hasattr(self, 'custom_title'):
4102 self.gPodderOpmlLister.set_title(self.custom_title)
4103 if hasattr(self, 'hide_url_entry'):
4104 self.hboxOpmlUrlEntry.hide_all()
4105 new_parent = self.notebookChannelAdder.get_parent()
4106 new_parent.remove(self.notebookChannelAdder)
4107 self.vboxOpmlImport.reparent(new_parent)
4109 self.setup_treeview(self.treeviewChannelChooser)
4110 self.setup_treeview(self.treeviewTopPodcastsChooser)
4111 self.setup_treeview(self.treeviewYouTubeChooser)
4113 self.current_mode = self.MODE_DOWNLOAD
4115 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
4117 def setup_treeview(self, tv):
4118 togglecell = gtk.CellRendererToggle()
4119 togglecell.set_property( 'activatable', True)
4120 togglecell.connect( 'toggled', self.callback_edited)
4121 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
4123 titlecell = gtk.CellRendererText()
4124 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
4125 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
4127 for itemcolumn in ( togglecolumn, titlecolumn ):
4128 tv.append_column(itemcolumn)
4130 def callback_edited( self, cell, path):
4131 model = self.get_treeview().get_model()
4133 url = model[path][2]
4135 model[path][0] = not model[path][0]
4136 if model[path][0]:
4137 self.channels.append( url)
4138 else:
4139 self.channels.remove( url)
4141 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
4143 def on_entryURL_changed(self, editable):
4144 old_mode = self.current_mode
4145 self.current_mode = not editable.get_text().lower().startswith('http://')
4146 if self.current_mode == old_mode:
4147 return
4149 if self.current_mode == self.MODE_SEARCH:
4150 self.btnDownloadOpml.set_property('image', None)
4151 self.btnDownloadOpml.set_label(gtk.STOCK_FIND)
4152 self.btnDownloadOpml.set_use_stock(True)
4153 self.labelOpmlUrl.set_text(_('Search podcast.de:'))
4154 else:
4155 self.btnDownloadOpml.set_label(_('Download'))
4156 self.btnDownloadOpml.set_image(gtk.image_new_from_stock(gtk.STOCK_GOTO_BOTTOM, gtk.ICON_SIZE_BUTTON))
4157 self.btnDownloadOpml.set_use_stock(False)
4158 self.labelOpmlUrl.set_text(_('OPML:'))
4160 def get_selected_channels(self, tab=None):
4161 channels = []
4163 model = self.get_treeview(tab).get_model()
4164 if model is not None:
4165 for row in model:
4166 if row[0]:
4167 channels.append(row[2])
4169 return channels
4171 def on_change_tab(self, tab):
4172 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
4174 def thread_finished(self, model, tab=0):
4175 if tab == 1:
4176 tv = self.treeviewTopPodcastsChooser
4177 elif tab == 2:
4178 tv = self.treeviewYouTubeChooser
4179 self.entryYoutubeSearch.set_sensitive(True)
4180 self.btnSearchYouTube.set_sensitive(True)
4181 self.btnOK.set_sensitive(False)
4182 else:
4183 tv = self.treeviewChannelChooser
4184 self.btnDownloadOpml.set_sensitive(True)
4185 self.entryURL.set_sensitive(True)
4186 self.channels = []
4188 tv.set_model(model)
4189 tv.set_sensitive(True)
4191 def thread_func(self, tab=0):
4192 if tab == 1:
4193 model = opml.Importer(gl.config.toplist_url).get_model()
4194 if len(model) == 0:
4195 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4196 elif tab == 2:
4197 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
4198 if len(model) == 0:
4199 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
4200 else:
4201 url = self.entryURL.get_text()
4202 if not os.path.isfile(url) and not url.lower().startswith('http://'):
4203 log('Using podcast.de search')
4204 url = 'http://api.podcast.de/opml/podcasts/suche/%s' % (urllib.quote(url),)
4205 model = opml.Importer(url).get_model()
4206 if len(model) == 0:
4207 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4209 util.idle_add(self.thread_finished, model, tab)
4211 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
4212 if callback_for_channel:
4213 self.callback_for_channel = callback_for_channel
4214 if callback_finished:
4215 self.callback_finished = callback_finished
4216 self.entryURL.set_text( url)
4217 self.btnDownloadOpml.set_sensitive( False)
4218 self.entryURL.set_sensitive( False)
4219 self.btnOK.set_sensitive( False)
4220 self.treeviewChannelChooser.set_sensitive( False)
4221 Thread( target = self.thread_func).start()
4222 Thread( target = lambda: self.thread_func(1)).start()
4224 def select_all( self, value ):
4225 enabled = False
4226 model = self.get_treeview().get_model()
4227 if model is not None:
4228 for row in model:
4229 row[0] = value
4230 if value:
4231 enabled = True
4232 self.btnOK.set_sensitive(enabled)
4234 def on_gPodderOpmlLister_destroy(self, widget, *args):
4235 pass
4237 def on_btnDownloadOpml_clicked(self, widget, *args):
4238 self.get_channels_from_url( self.entryURL.get_text())
4240 def on_btnSearchYouTube_clicked(self, widget, *args):
4241 self.entryYoutubeSearch.set_sensitive(False)
4242 self.treeviewYouTubeChooser.set_sensitive(False)
4243 self.btnSearchYouTube.set_sensitive(False)
4244 Thread(target = lambda: self.thread_func(2)).start()
4246 def on_btnSelectAll_clicked(self, widget, *args):
4247 self.select_all(True)
4249 def on_btnSelectNone_clicked(self, widget, *args):
4250 self.select_all(False)
4252 def on_btnOK_clicked(self, widget, *args):
4253 self.channels = self.get_selected_channels()
4254 self.gPodderOpmlLister.destroy()
4256 # add channels that have been selected
4257 for url in self.channels:
4258 if self.callback_for_channel:
4259 self.callback_for_channel( url)
4261 if self.callback_finished:
4262 util.idle_add(self.callback_finished)
4264 def on_btnCancel_clicked(self, widget, *args):
4265 self.gPodderOpmlLister.destroy()
4267 def on_entryYoutubeSearch_key_press_event(self, widget, event):
4268 if event.keyval == gtk.keysyms.Return:
4269 self.on_btnSearchYouTube_clicked(widget)
4271 def get_treeview(self, tab=None):
4272 if tab is None:
4273 tab = self.notebookChannelAdder.get_current_page()
4275 if tab == 0:
4276 return self.treeviewChannelChooser
4277 elif tab == 1:
4278 return self.treeviewTopPodcastsChooser
4279 else:
4280 return self.treeviewYouTubeChooser
4282 class gPodderEpisodeSelector( BuilderWidget):
4283 """Episode selection dialog
4285 Optional keyword arguments that modify the behaviour of this dialog:
4287 - callback: Function that takes 1 parameter which is a list of
4288 the selected episodes (or empty list when none selected)
4289 - remove_callback: Function that takes 1 parameter which is a list
4290 of episodes that should be "removed" (see below)
4291 (default is None, which means remove not possible)
4292 - remove_action: Label for the "remove" action (default is "Remove")
4293 - remove_finished: Callback after all remove callbacks have finished
4294 (default is None, also depends on remove_callback)
4295 It will get a list of episode URLs that have been
4296 removed, so the main UI can update those
4297 - episodes: List of episodes that are presented for selection
4298 - selected: (optional) List of boolean variables that define the
4299 default checked state for the given episodes
4300 - selected_default: (optional) The default boolean value for the
4301 checked state if no other value is set
4302 (default is False)
4303 - columns: List of (name, sort_name, sort_type, caption) pairs for the
4304 columns, the name is the attribute name of the episode to be
4305 read from each episode object. The sort name is the
4306 attribute name of the episode to be used to sort this column.
4307 If the sort_name is None it will use the attribute name for
4308 sorting. The sort type is the type of the sort column.
4309 The caption attribute is the text that appear as column caption
4310 (default is [('title_and_description', None, None, 'Episode'),])
4311 - title: (optional) The title of the window + heading
4312 - instructions: (optional) A one-line text describing what the
4313 user should select / what the selection is for
4314 - stock_ok_button: (optional) Will replace the "OK" button with
4315 another GTK+ stock item to be used for the
4316 affirmative button of the dialog (e.g. can
4317 be gtk.STOCK_DELETE when the episodes to be
4318 selected will be deleted after closing the
4319 dialog)
4320 - selection_buttons: (optional) A dictionary with labels as
4321 keys and callbacks as values; for each
4322 key a button will be generated, and when
4323 the button is clicked, the callback will
4324 be called for each episode and the return
4325 value of the callback (True or False) will
4326 be the new selected state of the episode
4327 - size_attribute: (optional) The name of an attribute of the
4328 supplied episode objects that can be used to
4329 calculate the size of an episode; set this to
4330 None if no total size calculation should be
4331 done (in cases where total size is useless)
4332 (default is 'length')
4333 - tooltip_attribute: (optional) The name of an attribute of
4334 the supplied episode objects that holds
4335 the text for the tooltips when hovering
4336 over an episode (default is 'description')
4339 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
4341 COLUMN_INDEX = 0
4342 COLUMN_TOOLTIP = 1
4343 COLUMN_TOGGLE = 2
4344 COLUMN_ADDITIONAL = 3
4346 def new( self):
4347 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
4348 if not hasattr( self, 'callback'):
4349 self.callback = None
4351 if not hasattr(self, 'remove_callback'):
4352 self.remove_callback = None
4354 if not hasattr(self, 'remove_action'):
4355 self.remove_action = _('Remove')
4357 if not hasattr(self, 'remove_finished'):
4358 self.remove_finished = None
4360 if not hasattr( self, 'episodes'):
4361 self.episodes = []
4363 if not hasattr( self, 'size_attribute'):
4364 self.size_attribute = 'length'
4366 if not hasattr(self, 'tooltip_attribute'):
4367 self.tooltip_attribute = 'description'
4369 if not hasattr( self, 'selection_buttons'):
4370 self.selection_buttons = {}
4372 if not hasattr( self, 'selected_default'):
4373 self.selected_default = False
4375 if not hasattr( self, 'selected'):
4376 self.selected = [self.selected_default]*len(self.episodes)
4378 if len(self.selected) < len(self.episodes):
4379 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
4381 if not hasattr( self, 'columns'):
4382 self.columns = (('title_and_description', None, None, _('Episode')),)
4384 if hasattr( self, 'title'):
4385 self.gPodderEpisodeSelector.set_title( self.title)
4386 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
4388 if gpodder.interface == gpodder.MAEMO:
4389 self.labelHeading.hide()
4391 if hasattr( self, 'instructions'):
4392 self.labelInstructions.set_text( self.instructions)
4393 self.labelInstructions.show_all()
4395 if hasattr(self, 'stock_ok_button'):
4396 if self.stock_ok_button == 'gpodder-download':
4397 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
4398 self.btnOK.set_label(_('Download'))
4399 else:
4400 self.btnOK.set_label(self.stock_ok_button)
4401 self.btnOK.set_use_stock(True)
4403 # check/uncheck column
4404 toggle_cell = gtk.CellRendererToggle()
4405 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
4406 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
4408 next_column = self.COLUMN_ADDITIONAL
4409 for name, sort_name, sort_type, caption in self.columns:
4410 renderer = gtk.CellRendererText()
4411 if next_column < self.COLUMN_ADDITIONAL + 2:
4412 renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
4413 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
4414 column.set_resizable( True)
4415 # Only set "expand" on the first two columns
4416 if next_column < self.COLUMN_ADDITIONAL + 2:
4417 column.set_expand(True)
4418 if sort_name is not None:
4419 column.set_sort_column_id(next_column+1)
4420 else:
4421 column.set_sort_column_id(next_column)
4422 self.treeviewEpisodes.append_column( column)
4423 next_column += 1
4425 if sort_name is not None:
4426 # add the sort column
4427 column = gtk.TreeViewColumn()
4428 column.set_visible(False)
4429 self.treeviewEpisodes.append_column( column)
4430 next_column += 1
4432 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
4433 # add string column type plus sort column type if it exists
4434 for name, sort_name, sort_type, caption in self.columns:
4435 column_types.append(gobject.TYPE_STRING)
4436 if sort_name is not None:
4437 column_types.append(sort_type)
4438 self.model = gtk.ListStore( *column_types)
4440 tooltip = None
4441 for index, episode in enumerate( self.episodes):
4442 if self.tooltip_attribute is not None:
4443 try:
4444 tooltip = getattr(episode, self.tooltip_attribute)
4445 except:
4446 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
4447 tooltip = None
4448 row = [ index, tooltip, self.selected[index] ]
4449 for name, sort_name, sort_type, caption in self.columns:
4450 if not hasattr(episode, name):
4451 log('Warning: Missing attribute "%s"', name, sender=self)
4452 row.append(None)
4453 else:
4454 row.append(getattr( episode, name))
4456 if sort_name is not None:
4457 if not hasattr(episode, sort_name):
4458 log('Warning: Missing attribute "%s"', sort_name, sender=self)
4459 row.append(None)
4460 else:
4461 row.append(getattr( episode, sort_name))
4462 self.model.append( row)
4464 if self.remove_callback is not None:
4465 self.btnRemoveAction.show()
4466 self.btnRemoveAction.set_label(self.remove_action)
4468 # connect to tooltip signals
4469 if self.tooltip_attribute is not None:
4470 try:
4471 self.treeviewEpisodes.set_property('has-tooltip', True)
4472 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
4473 except:
4474 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
4475 self.last_tooltip_episode = None
4476 self.episode_list_can_tooltip = True
4478 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
4479 self.treeviewEpisodes.set_rules_hint( True)
4480 self.treeviewEpisodes.set_model( self.model)
4481 self.treeviewEpisodes.columns_autosize()
4482 self.calculate_total_size()
4484 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
4485 # With get_bin_window, we get the window that contains the rows without
4486 # the header. The Y coordinate of this window will be the height of the
4487 # treeview header. This is the amount we have to subtract from the
4488 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
4489 (x_bin, y_bin) = treeview.get_bin_window().get_position()
4490 y -= x_bin
4491 y -= y_bin
4492 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
4494 if not self.episode_list_can_tooltip:
4495 self.last_tooltip_episode = None
4496 return False
4498 if path is not None:
4499 model = treeview.get_model()
4500 iter = model.get_iter(path)
4501 index = model.get_value(iter, self.COLUMN_INDEX)
4502 description = model.get_value(iter, self.COLUMN_TOOLTIP)
4503 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
4504 self.last_tooltip_episode = None
4505 return False
4506 self.last_tooltip_episode = index
4508 if description is not None:
4509 tooltip.set_text(description)
4510 return True
4511 else:
4512 return False
4514 self.last_tooltip_episode = None
4515 return False
4517 def treeview_episodes_button_pressed(self, treeview, event):
4518 if event.button == 3:
4519 menu = gtk.Menu()
4521 if len(self.selection_buttons):
4522 for label in self.selection_buttons:
4523 item = gtk.MenuItem(label)
4524 item.connect('activate', self.custom_selection_button_clicked, label)
4525 menu.append(item)
4526 menu.append(gtk.SeparatorMenuItem())
4528 item = gtk.MenuItem(_('Select all'))
4529 item.connect('activate', self.on_btnCheckAll_clicked)
4530 menu.append(item)
4532 item = gtk.MenuItem(_('Select none'))
4533 item.connect('activate', self.on_btnCheckNone_clicked)
4534 menu.append(item)
4536 menu.show_all()
4537 # Disable tooltips while we are showing the menu, so
4538 # the tooltip will not appear over the menu
4539 self.episode_list_can_tooltip = False
4540 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
4541 menu.popup(None, None, None, event.button, event.time)
4543 return True
4545 def episode_list_allow_tooltips(self):
4546 self.episode_list_can_tooltip = True
4548 def calculate_total_size( self):
4549 if self.size_attribute is not None:
4550 (total_size, count) = (0, 0)
4551 for episode in self.get_selected_episodes():
4552 try:
4553 total_size += int(getattr( episode, self.size_attribute))
4554 count += 1
4555 except:
4556 log( 'Cannot get size for %s', episode.title, sender = self)
4558 text = []
4559 if count == 0:
4560 text.append(_('Nothing selected'))
4561 elif count == 1:
4562 text.append(_('One episode selected'))
4563 else:
4564 text.append(_('%d episodes selected') % count)
4565 if total_size > 0:
4566 text.append(_('total size: %s') % gl.format_filesize(total_size))
4567 self.labelTotalSize.set_text(', '.join(text))
4568 self.btnOK.set_sensitive(count>0)
4569 self.btnRemoveAction.set_sensitive(count>0)
4570 if count > 0:
4571 self.btnCancel.set_label(gtk.STOCK_CANCEL)
4572 else:
4573 self.btnCancel.set_label(gtk.STOCK_CLOSE)
4574 else:
4575 self.btnOK.set_sensitive(False)
4576 self.btnRemoveAction.set_sensitive(False)
4577 for index, row in enumerate(self.model):
4578 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
4579 self.btnOK.set_sensitive(True)
4580 self.btnRemoveAction.set_sensitive(True)
4581 break
4582 self.labelTotalSize.set_text('')
4584 def toggle_cell_handler( self, cell, path):
4585 model = self.treeviewEpisodes.get_model()
4586 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
4588 self.calculate_total_size()
4590 def custom_selection_button_clicked(self, button, label):
4591 callback = self.selection_buttons[label]
4593 for index, row in enumerate( self.model):
4594 new_value = callback( self.episodes[index])
4595 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
4597 self.calculate_total_size()
4599 def on_btnCheckAll_clicked( self, widget):
4600 for row in self.model:
4601 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
4603 self.calculate_total_size()
4605 def on_btnCheckNone_clicked( self, widget):
4606 for row in self.model:
4607 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
4609 self.calculate_total_size()
4611 def on_remove_action_activate(self, widget):
4612 episodes = self.get_selected_episodes(remove_episodes=True)
4614 urls = []
4615 for episode in episodes:
4616 urls.append(episode.url)
4617 self.remove_callback(episode)
4619 if self.remove_finished is not None:
4620 self.remove_finished(urls)
4621 self.calculate_total_size()
4623 def get_selected_episodes( self, remove_episodes=False):
4624 selected_episodes = []
4626 for index, row in enumerate( self.model):
4627 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
4628 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
4630 if remove_episodes:
4631 for episode in selected_episodes:
4632 index = self.episodes.index(episode)
4633 iter = self.model.get_iter_first()
4634 while iter is not None:
4635 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
4636 self.model.remove(iter)
4637 break
4638 iter = self.model.iter_next(iter)
4640 return selected_episodes
4642 def on_btnOK_clicked( self, widget):
4643 self.gPodderEpisodeSelector.destroy()
4644 if self.callback is not None:
4645 self.callback( self.get_selected_episodes())
4647 def on_btnCancel_clicked( self, widget):
4648 self.gPodderEpisodeSelector.destroy()
4649 if self.callback is not None:
4650 self.callback([])
4652 class gPodderConfigEditor(BuilderWidget):
4653 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
4655 def new(self):
4656 name_column = gtk.TreeViewColumn(_('Setting'))
4657 name_renderer = gtk.CellRendererText()
4658 name_column.pack_start(name_renderer)
4659 name_column.add_attribute(name_renderer, 'text', 0)
4660 name_column.add_attribute(name_renderer, 'style', 5)
4661 self.configeditor.append_column(name_column)
4663 value_column = gtk.TreeViewColumn(_('Set to'))
4664 value_check_renderer = gtk.CellRendererToggle()
4665 value_column.pack_start(value_check_renderer, expand=False)
4666 value_column.add_attribute(value_check_renderer, 'active', 7)
4667 value_column.add_attribute(value_check_renderer, 'visible', 6)
4668 value_column.add_attribute(value_check_renderer, 'activatable', 6)
4669 value_check_renderer.connect('toggled', self.value_toggled)
4671 value_renderer = gtk.CellRendererText()
4672 value_column.pack_start(value_renderer)
4673 value_column.add_attribute(value_renderer, 'text', 2)
4674 value_column.add_attribute(value_renderer, 'visible', 4)
4675 value_column.add_attribute(value_renderer, 'editable', 4)
4676 value_column.add_attribute(value_renderer, 'style', 5)
4677 value_renderer.connect('edited', self.value_edited)
4678 self.configeditor.append_column(value_column)
4680 self.model = gl.config.model()
4681 self.filter = self.model.filter_new()
4682 self.filter.set_visible_func(self.visible_func)
4684 self.configeditor.set_model(self.filter)
4685 self.configeditor.set_rules_hint(True)
4686 self.configeditor.get_selection().connect( 'changed',
4687 self.on_configeditor_row_changed )
4689 def visible_func(self, model, iter, user_data=None):
4690 text = self.entryFilter.get_text().lower()
4691 if text == '':
4692 return True
4693 else:
4694 # either the variable name or its value
4695 return (text in model.get_value(iter, 0).lower() or
4696 text in model.get_value(iter, 2).lower())
4698 def value_edited(self, renderer, path, new_text):
4699 model = self.configeditor.get_model()
4700 iter = model.get_iter(path)
4701 name = model.get_value(iter, 0)
4702 type_cute = model.get_value(iter, 1)
4704 if not gl.config.update_field(name, new_text):
4705 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))
4707 def value_toggled(self, renderer, path):
4708 model = self.configeditor.get_model()
4709 iter = model.get_iter(path)
4710 field_name = model.get_value(iter, 0)
4711 field_type = model.get_value(iter, 3)
4713 # Flip the boolean config flag
4714 if field_type == bool:
4715 gl.config.toggle_flag(field_name)
4717 def on_entryFilter_changed(self, widget):
4718 self.filter.refilter()
4720 def on_btnShowAll_clicked(self, widget):
4721 self.entryFilter.set_text('')
4722 self.entryFilter.grab_focus()
4724 def on_btnClose_clicked(self, widget):
4725 self.gPodderConfigEditor.destroy()
4727 def on_configeditor_row_changed(self, treeselection):
4728 model, iter = treeselection.get_selected()
4729 if iter is not None:
4730 option_name = gl.config.get_description( model.get(iter, 0)[0] )
4731 self.config_option_description_label.set_text(option_name)
4733 class gPodderPlaylist(BuilderWidget):
4734 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4736 def new(self):
4737 self.linebreak = '\n'
4738 if gl.config.mp3_player_playlist_win_path:
4739 self.linebreak = '\r\n'
4740 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
4741 if self.mountpoint == '/':
4742 self.mountpoint = gl.config.mp3_player_folder
4743 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
4744 self.playlist_file = os.path.join(self.mountpoint,
4745 gl.config.mp3_player_playlist_file)
4746 icon_theme = gtk.icon_theme_get_default()
4747 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4749 # add column two
4750 check_cell = gtk.CellRendererToggle()
4751 check_cell.set_property('activatable', True)
4752 check_cell.connect('toggled', self.cell_toggled)
4753 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4754 self.treeviewPlaylist.append_column(check_column)
4756 # add column three
4757 column = gtk.TreeViewColumn(_('Filename'))
4758 icon_cell = gtk.CellRendererPixbuf()
4759 column.pack_start(icon_cell, False)
4760 column.add_attribute(icon_cell, 'pixbuf', 0)
4761 filename_cell = gtk.CellRendererText()
4762 column.pack_start(filename_cell, True)
4763 column.add_attribute(filename_cell, 'text', 2)
4765 column.set_resizable(True)
4766 self.treeviewPlaylist.append_column(column)
4768 # Make treeview reorderable
4769 self.treeviewPlaylist.set_reorderable(True)
4771 # init liststore
4772 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4773 self.treeviewPlaylist.set_model(self.playlist)
4775 # read device and playlist and fill the TreeView
4776 title = _('Reading files from %s') % gl.config.mp3_player_folder
4777 message = _('Please wait while gPodder reads your media file list from device.')
4778 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
4779 dlg.set_title(title)
4780 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
4781 dlg.show_all()
4782 Thread(target=self.process_device, args=[dlg]).start()
4784 def process_device(self, dlg):
4785 self.m3u = self.read_m3u()
4786 self.device = self.read_device()
4787 util.idle_add(self.write2gui, dlg)
4789 def cell_toggled(self, cellrenderertoggle, path):
4790 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4791 it = liststore.get_iter(path)
4792 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4794 def on_btnCancelPlaylist_clicked(self, widget):
4795 self.gPodderPlaylist.destroy()
4797 def on_btnSavePlaylist_clicked(self, widget):
4798 self.write_m3u()
4799 self.gPodderPlaylist.destroy()
4801 def read_m3u(self):
4803 read all files from the existing playlist
4805 tracks = []
4806 log("Read data from the playlistfile %s" % self.playlist_file)
4807 if os.path.exists(self.playlist_file):
4808 for line in open(self.playlist_file, 'r'):
4809 if not line.startswith('#EXT'):
4810 if line.startswith('#'):
4811 tracks.append([False, line[1:].strip()])
4812 else:
4813 tracks.append([True, line.strip()])
4814 return tracks
4816 def build_extinf(self, filename):
4817 if gl.config.mp3_player_playlist_win_path:
4818 filename = filename.replace('\\', os.sep)
4820 # rebuild the whole filename including the mountpoint
4821 if gl.config.mp3_player_playlist_absolute_path:
4822 absfile = self.mountpoint + filename
4823 else:
4824 absfile = util.rel2abs(filename, os.path.dirname(self.playlist_file))
4826 # read the title from the mp3/ogg tag
4827 metadata = libtagupdate.get_tags_from_file(absfile)
4828 if 'title' in metadata and metadata['title']:
4829 title = metadata['title']
4830 else:
4831 # fallback: use the basename of the file
4832 (title, extension) = os.path.splitext(os.path.basename(filename))
4834 return "#EXTINF:0,%s%s" % (title.strip(), self.linebreak)
4836 def write_m3u(self):
4838 write the list into the playlist on the device
4840 log('Writing playlist file: %s', self.playlist_file, sender=self)
4841 playlist_folder = os.path.split(self.playlist_file)[0]
4842 if not util.make_directory(playlist_folder):
4843 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4844 else:
4845 try:
4846 fp = open(self.playlist_file, 'w')
4847 fp.write('#EXTM3U%s' % self.linebreak)
4848 for icon, checked, filename in self.playlist:
4849 fp.write(self.build_extinf(filename))
4850 if not checked:
4851 fp.write('#')
4852 fp.write(filename)
4853 fp.write(self.linebreak)
4854 fp.close()
4855 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4856 except IOError, ioe:
4857 self.show_message(str(ioe), _('Error writing playlist file'))
4859 def read_device(self):
4861 read all files from the device
4863 log('Reading files from %s', gl.config.mp3_player_folder, sender=self)
4864 tracks = []
4865 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4866 for file in files:
4867 filename = os.path.join(root, file)
4869 if filename == self.playlist_file:
4870 # We don't want to have our playlist file as
4871 # an entry in our file list, so skip it!
4872 break
4874 if gl.config.mp3_player_playlist_absolute_path:
4875 filename = filename[len(self.mountpoint):]
4876 else:
4877 filename = util.relpath(os.path.dirname(self.playlist_file),
4878 os.path.dirname(filename)) + \
4879 os.sep + os.path.basename(filename)
4881 if gl.config.mp3_player_playlist_win_path:
4882 filename = filename.replace(os.sep, '\\')
4884 tracks.append(filename)
4885 return tracks
4887 def write2gui(self, dlg):
4888 # add the files from the device to the list only when
4889 # they are not yet in the playlist
4890 # mark this files as NEW
4891 for filename in self.device[:]:
4892 m3ulist = [file[1] for file in self.m3u]
4893 if filename not in m3ulist:
4894 self.playlist.append([self.icon_new, False, filename])
4896 # add the files from the playlist to the list only when
4897 # they are on the device
4898 for checked, filename in self.m3u[:]:
4899 if filename in self.device:
4900 self.playlist.append([None, checked, filename])
4902 dlg.destroy()
4903 return False
4905 class gPodderDependencyManager(BuilderWidget):
4906 def new(self):
4907 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4908 self.treeview_components.append_column(col_name)
4909 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4910 self.treeview_components.append_column(col_installed)
4911 self.treeview_components.set_model(services.dependency_manager.get_model())
4912 self.btn_about.set_sensitive(False)
4914 def on_btn_about_clicked(self, widget):
4915 selection = self.treeview_components.get_selection()
4916 model, iter = selection.get_selected()
4917 if iter is not None:
4918 title = model.get_value(iter, 0)
4919 description = model.get_value(iter, 1)
4920 available = model.get_value(iter, 3)
4921 missing = model.get_value(iter, 4)
4923 if not available:
4924 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4926 self.show_message(description, title)
4928 def on_btn_install_clicked(self, widget):
4929 # TODO: Implement package manager integration
4930 pass
4932 def on_treeview_components_cursor_changed(self, treeview):
4933 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4934 # TODO: If installing is possible, enable btn_install
4936 def on_gPodderDependencyManager_response(self, dialog, response_id):
4937 self.gPodderDependencyManager.destroy()
4939 class gPodderWelcome(BuilderWidget):
4940 finger_friendly_widgets = ['btnOPML', 'btnMygPodder', 'btnCancel']
4942 def new(self):
4943 pass
4945 def on_show_example_podcasts(self, button):
4946 self.gPodderWelcome.destroy()
4947 self.show_example_podcasts_callback(None)
4949 def on_setup_my_gpodder(self, gpodder):
4950 self.gPodderWelcome.destroy()
4951 self.setup_my_gpodder_callback(None)
4953 def on_btnCancel_clicked(self, button):
4954 self.gPodderWelcome.destroy()
4956 def main():
4957 gobject.threads_init()
4958 gtk.window_set_default_icon_name( 'gpodder')
4960 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
4961 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
4962 gp = gPodder(bus_name)
4963 gp.run()