Update podcast list after download status changes (bug 433)
[gpodder.git] / src / gpodder / gui.py
blob134e46d671c58bdfb05ef7489b6498b20c7f9b6a
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 widget is None:
209 return None
211 if gpodder.interface == gpodder.MAEMO:
212 if isinstance(widget, gtk.Misc):
213 widget.set_padding(0, 5)
214 elif isinstance(widget, gtk.Button):
215 for child in widget.get_children():
216 if isinstance(child, gtk.Alignment):
217 child.set_padding(5, 5, 5, 5)
218 else:
219 child.set_padding(5, 5)
220 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
221 parent = widget.get_parent()
222 if isinstance(parent, gtk.ScrolledWindow):
223 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
224 elif isinstance(widget, gtk.MenuItem):
225 for child in widget.get_children():
226 self.set_finger_friendly(child)
227 submenu = widget.get_submenu()
228 if submenu is not None:
229 for child in submenu.get_children():
230 self.set_finger_friendly(child)
231 elif isinstance(widget, gtk.Menu):
232 for child in widget.get_children():
233 self.set_finger_friendly(child)
234 else:
235 log('Cannot set widget finger-friendly: %s', widget, sender=self)
237 return widget
239 def show_confirmation( self, message, title = None):
240 if gpodder.interface == gpodder.GUI:
241 affirmative = gtk.RESPONSE_YES
242 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
243 if title:
244 dlg.set_title(str(title))
245 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
246 else:
247 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
248 elif gpodder.interface == gpodder.MAEMO:
249 affirmative = gtk.RESPONSE_OK
250 dlg = hildon.Note('confirmation', (BuilderWidget.gpodder_main_window, message))
252 response = dlg.run()
253 dlg.destroy()
255 return response == affirmative
257 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
258 """ An authentication dialog based on
259 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
261 dialog = gtk.MessageDialog(
262 BuilderWidget.gpodder_main_window,
263 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
264 gtk.MESSAGE_QUESTION,
265 gtk.BUTTONS_OK_CANCEL )
267 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
269 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
270 dialog.set_title(_('Authentication required'))
271 dialog.format_secondary_markup(message)
272 dialog.set_default_response(gtk.RESPONSE_OK)
274 if register_callback is not None:
275 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
277 username_entry = gtk.Entry()
278 password_entry = gtk.Entry()
280 username_entry.connect('activate', lambda w: password_entry.grab_focus())
281 password_entry.set_visibility(False)
282 password_entry.set_activates_default(True)
284 if username is not None:
285 username_entry.set_text(username)
286 if password is not None:
287 password_entry.set_text(password)
289 table = gtk.Table(2, 2)
290 table.set_row_spacings(6)
291 table.set_col_spacings(6)
293 username_label = gtk.Label()
294 username_label.set_markup('<b>' + username_prompt + ':</b>')
295 username_label.set_alignment(0.0, 0.5)
296 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
297 table.attach(username_entry, 1, 2, 0, 1)
299 password_label = gtk.Label()
300 password_label.set_markup('<b>' + _('Password') + ':</b>')
301 password_label.set_alignment(0.0, 0.5)
302 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
303 table.attach(password_entry, 1, 2, 1, 2)
305 dialog.vbox.pack_end(table, True, True, 0)
306 dialog.show_all()
307 response = dialog.run()
309 while response == gtk.RESPONSE_HELP:
310 register_callback()
311 response = dialog.run()
313 password_entry.set_visibility(True)
314 dialog.destroy()
316 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
318 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
319 if dst_filename is None:
320 dst_filename = src_filename
322 if dst_directory is None:
323 dst_directory = os.path.expanduser( '~')
325 ( base, extension ) = os.path.splitext( src_filename)
327 if not dst_filename.endswith( extension):
328 dst_filename += extension
330 if gpodder.interface == gpodder.GUI:
331 dlg = gtk.FileChooserDialog(title=title, parent=BuilderWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
332 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
333 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
334 elif gpodder.interface == gpodder.MAEMO:
335 dlg = hildon.FileChooserDialog(BuilderWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
337 dlg.set_do_overwrite_confirmation( True)
338 dlg.set_current_name( os.path.basename( dst_filename))
339 dlg.set_current_folder( dst_directory)
341 result = False
342 folder = dst_directory
343 if dlg.run() == gtk.RESPONSE_OK:
344 result = True
345 dst_filename = dlg.get_filename()
346 folder = dlg.get_current_folder()
347 if not dst_filename.endswith( extension):
348 dst_filename += extension
350 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
352 try:
353 shutil.copyfile( src_filename, dst_filename)
354 except:
355 log( 'Error copying file.', sender = self, traceback = True)
357 dlg.destroy()
358 return (result, folder)
361 class gPodder(BuilderWidget, dbus.service.Object):
362 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads']
363 ENTER_URL_TEXT = _('Enter podcast URL...')
365 def __init__(self, bus_name):
366 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
367 BuilderWidget.__init__(self)
369 def new(self):
370 if gpodder.interface == gpodder.MAEMO:
371 # Maemo-specific changes to the UI
372 gpodder.icon_file = gpodder.icon_file.replace('.svg', '.png')
374 self.app = hildon.Program()
375 gtk.set_application_name('gPodder')
376 self.window = hildon.Window()
377 self.window.connect('delete-event', self.on_gPodder_delete_event)
378 self.window.connect('window-state-event', self.window_state_event)
380 self.itemUpdateChannel.set_visible(True)
382 # Remove old toolbar from its parent widget
383 self.toolbar.get_parent().remove(self.toolbar)
385 toolbar = gtk.Toolbar()
386 toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
388 self.btnUpdateFeeds.get_parent().remove(self.btnUpdateFeeds)
390 self.btnUpdateFeeds = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update all'))
391 self.btnUpdateFeeds.set_is_important(True)
392 self.btnUpdateFeeds.connect('clicked', self.on_itemUpdate_activate)
393 toolbar.insert(self.btnUpdateFeeds, -1)
394 self.btnUpdateFeeds.show_all()
396 self.btnUpdateSelectedFeed = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update selected'))
397 self.btnUpdateSelectedFeed.set_is_important(True)
398 self.btnUpdateSelectedFeed.connect('clicked', self.on_itemUpdateChannel_activate)
399 toolbar.insert(self.btnUpdateSelectedFeed, -1)
400 self.btnUpdateSelectedFeed.show_all()
402 self.toolFeedUpdateProgress = gtk.ToolItem()
403 self.pbFeedUpdate.reparent(self.toolFeedUpdateProgress)
404 self.toolFeedUpdateProgress.set_expand(True)
405 toolbar.insert(self.toolFeedUpdateProgress, -1)
406 self.toolFeedUpdateProgress.hide()
408 self.btnCancelFeedUpdate = gtk.ToolButton(gtk.STOCK_CLOSE)
409 self.btnCancelFeedUpdate.connect('clicked', self.on_btnCancelFeedUpdate_clicked)
410 toolbar.insert(self.btnCancelFeedUpdate, -1)
411 self.btnCancelFeedUpdate.hide()
413 self.toolbarSpacer = gtk.SeparatorToolItem()
414 self.toolbarSpacer.set_draw(False)
415 self.toolbarSpacer.set_expand(True)
416 toolbar.insert(self.toolbarSpacer, -1)
417 self.toolbarSpacer.show()
419 self.wNotebook.set_show_tabs(False)
420 self.tool_downloads = gtk.ToggleToolButton(gtk.STOCK_GO_DOWN)
421 self.tool_downloads.connect('toggled', self.on_tool_downloads_toggled)
422 self.tool_downloads.set_label(_('Downloads'))
423 self.tool_downloads.set_is_important(True)
424 toolbar.insert(self.tool_downloads, -1)
425 self.tool_downloads.show_all()
427 self.toolPreferences = gtk.ToolButton(gtk.STOCK_PREFERENCES)
428 self.toolPreferences.connect('clicked', self.on_itemPreferences_activate)
429 toolbar.insert(self.toolPreferences, -1)
430 self.toolPreferences.show()
432 self.toolQuit = gtk.ToolButton(gtk.STOCK_QUIT)
433 self.toolQuit.connect('clicked', self.on_gPodder_delete_event)
434 toolbar.insert(self.toolQuit, -1)
435 self.toolQuit.show()
437 # Add and replace toolbar with our new one
438 toolbar.show()
439 self.window.add_toolbar(toolbar)
440 self.toolbar = toolbar
442 self.app.add_window(self.window)
443 self.vMain.reparent(self.window)
444 self.gPodder = self.window
446 # Reparent the main menu
447 menu = gtk.Menu()
448 for child in self.mainMenu.get_children():
449 child.get_parent().remove(child)
450 menu.append(self.set_finger_friendly(child))
451 menu.append(self.set_finger_friendly(self.itemQuit.create_menu_item()))
452 self.window.set_menu(menu)
454 self.mainMenu.destroy()
455 self.window.show()
457 # do some widget hiding
458 self.itemTransferSelected.set_visible(False)
459 self.item_email_subscriptions.set_visible(False)
460 self.menuView.set_visible(False)
462 # get screen real estate
463 self.hboxContainer.set_border_width(0)
465 # Offer importing of videocenter podcasts
466 if os.path.exists(os.path.expanduser('~/videocenter')):
467 self.item_upgrade_from_videocenter.show()
468 self.upgrade_from_videocenter_separator.show()
470 self.gPodder.connect('key-press-event', self.on_key_press)
471 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
473 if gpodder.win32:
474 # FIXME: Implement e-mail sending of list in win32
475 self.item_email_subscriptions.set_sensitive(False)
477 if gl.config.show_url_entry_in_podcast_list:
478 self.hboxAddChannel.show()
480 if not gpodder.interface == gpodder.MAEMO and not gl.config.show_toolbar:
481 self.toolbar.hide()
483 gl.config.add_observer(self.on_config_changed)
484 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
485 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
486 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
487 self.entry_add_channel_unfocus(self.entryAddChannel, None)
489 self.uar = None
490 self.tray_icon = None
491 self.gpodder_episode_window = None
493 self.download_status_manager = services.DownloadStatusManager()
494 self.download_queue_manager = download.DownloadQueueManager(self.download_status_manager)
496 self.fullscreen = False
497 self.minimized = False
498 self.gPodder.connect('window-state-event', self.window_state_event)
500 self.already_notified_new_episodes = []
501 self.show_hide_tray_icon()
503 self.itemShowToolbar.set_active(gl.config.show_toolbar)
504 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
506 gl.config.connect_gtk_window(self.gPodder, 'main_window')
507 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
509 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
510 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
511 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
512 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
514 # Then the amount of maximum downloads changes, notify the queue manager
515 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
516 self.spinMaxDownloads.connect('value-changed', changed_cb)
518 self.default_title = None
519 if gpodder.__version__.rfind('git') != -1:
520 self.set_title('gPodder %s' % gpodder.__version__)
521 else:
522 title = self.gPodder.get_title()
523 if title is not None:
524 self.set_title(title)
525 else:
526 self.set_title(_('gPodder'))
528 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
530 # cell renderers for channel tree
531 iconcolumn = gtk.TreeViewColumn('')
533 iconcell = gtk.CellRendererPixbuf()
534 iconcolumn.pack_start( iconcell, False)
535 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
536 self.cell_channel_icon = iconcell
538 namecolumn = gtk.TreeViewColumn('')
539 namecell = gtk.CellRendererText()
540 namecell.set_property('foreground-set', True)
541 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
542 namecolumn.pack_start( namecell, True)
543 namecolumn.add_attribute( namecell, 'markup', 2)
544 namecolumn.add_attribute( namecell, 'foreground', 8)
546 iconcell = gtk.CellRendererPixbuf()
547 iconcell.set_property('xalign', 1.0)
548 namecolumn.pack_start( iconcell, False)
549 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
550 namecolumn.add_attribute(iconcell, 'visible', 7)
551 self.cell_channel_pill = iconcell
553 self.treeChannels.set_enable_search(True)
554 self.treeChannels.set_search_column(1)
555 self.treeChannels.append_column(iconcolumn)
556 self.treeChannels.append_column(namecolumn)
557 self.treeChannels.set_headers_visible(False)
559 # enable alternating colors hint
560 self.treeAvailable.set_rules_hint( True)
561 self.treeChannels.set_rules_hint( True)
563 # connect to tooltip signals
564 try:
565 self.treeChannels.set_property('has-tooltip', True)
566 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
567 self.treeAvailable.set_property('has-tooltip', True)
568 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
569 except:
570 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
571 self.last_tooltip_channel = None
572 self.last_tooltip_episode = None
573 self.podcast_list_can_tooltip = True
574 self.episode_list_can_tooltip = True
576 self.currently_updating = False
578 # Add our context menu to treeAvailable
579 if gpodder.interface == gpodder.MAEMO:
580 self.treeview_available_buttonpress = (0, 0)
581 self.treeAvailable.connect('button-press-event', self.treeview_button_savepos)
582 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
584 self.treeview_channels_buttonpress = (0, 0)
585 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
586 self.treeChannels.connect('button-release-event', self.treeview_channels_button_released)
587 else:
588 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
589 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
591 self.treeDownloads.connect('button-press-event', self.treeview_downloads_button_pressed)
593 iconcell = gtk.CellRendererPixbuf()
594 if gpodder.interface == gpodder.MAEMO:
595 iconcell.set_fixed_size(-1, 52)
596 status_column_label = ''
597 else:
598 status_column_label = _('Status')
599 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
601 namecell = gtk.CellRendererText()
602 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
603 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
604 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
605 namecolumn.set_resizable(True)
606 namecolumn.set_expand(True)
608 sizecell = gtk.CellRendererText()
609 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
611 releasecell = gtk.CellRendererText()
612 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
614 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
615 itemcolumn.set_reorderable(gpodder.interface != gpodder.MAEMO)
616 self.treeAvailable.append_column(itemcolumn)
618 if gpodder.interface == gpodder.MAEMO:
619 # Due to screen space contraints, we
620 # hide these columns here by default
621 self.column_size = sizecolumn
622 self.column_released = releasecolumn
623 self.column_released.set_visible(False)
624 self.column_size.set_visible(False)
626 # enable search in treeavailable
627 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
629 # enable multiple selection support
630 if gpodder.interface == gpodder.MAEMO:
631 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_SINGLE)
632 else:
633 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
634 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
636 if hasattr(self.treeDownloads, 'set_rubber_banding'):
637 # Available in PyGTK 2.10 and above
638 self.treeDownloads.set_rubber_banding(True)
640 # columns and renderers for "download progress" tab
641 DownloadStatusManager = services.DownloadStatusManager
643 # First column: [ICON] Episodename
644 column = gtk.TreeViewColumn(_('Episode'))
646 cell = gtk.CellRendererPixbuf()
647 if gpodder.interface == gpodder.MAEMO:
648 cell.set_property('stock-size', gtk.ICON_SIZE_DIALOG)
649 else:
650 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
651 column.pack_start(cell, expand=False)
652 column.add_attribute(cell, 'stock-id', \
653 DownloadStatusManager.C_ICON_NAME)
655 cell = gtk.CellRendererText()
656 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
657 column.pack_start(cell, expand=True)
658 column.add_attribute(cell, 'text', DownloadStatusManager.C_NAME)
660 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
661 column.set_resizable(True)
662 column.set_expand(True)
663 self.treeDownloads.append_column(column)
665 # Second column: Progress
666 column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
667 value=DownloadStatusManager.C_PROGRESS, \
668 text=DownloadStatusManager.C_PROGRESS_TEXT)
669 self.treeDownloads.append_column(column)
671 # Third column: Size
672 if gpodder.interface != gpodder.MAEMO:
673 column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
674 text=DownloadStatusManager.C_SIZE_TEXT)
675 self.treeDownloads.append_column(column)
677 # Fourth column: Speed
678 column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
679 text=DownloadStatusManager.C_SPEED_TEXT)
680 self.treeDownloads.append_column(column)
682 # Fifth column: Status
683 column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
684 text=DownloadStatusManager.C_STATUS_TEXT)
685 self.treeDownloads.append_column(column)
687 # After we've set up most of the window, show it :)
688 if not gpodder.interface == gpodder.MAEMO:
689 self.gPodder.show()
691 if gl.config.start_iconified:
692 self.iconify_main_window()
693 if self.tray_icon and gl.config.minimize_to_tray:
694 self.tray_icon.set_visible(False)
696 # a dictionary that maps episode URLs to the current
697 # treeAvailable row numbers to generate tree paths
698 self.url_path_mapping = {}
700 # a dictionary that maps channel URLs to the current
701 # treeChannels row numbers to generate tree paths
702 self.channel_url_path_mapping = {}
704 services.cover_downloader.register('cover-available', self.cover_download_finished)
705 services.cover_downloader.register('cover-removed', self.cover_file_removed)
706 self.cover_cache = {}
708 self.treeDownloads.set_model(self.download_status_manager.get_tree_model())
709 gobject.timeout_add(1500, self.update_downloads_list)
710 self.download_tasks_seen = set()
711 self.last_download_count = 0
713 #Add Drag and Drop Support
714 flags = gtk.DEST_DEFAULT_ALL
715 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
716 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
717 self.treeChannels.drag_dest_set( flags, targets, actions)
718 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
720 # Subscribed channels
721 self.active_channel = None
722 self.channels = load_channels()
723 self.channel_list_changed = True
724 self.update_podcasts_tab()
726 # load list of user applications for audio playback
727 self.user_apps_reader = UserAppsReader(['audio', 'video'])
728 Thread(target=self.read_apps).start()
730 # Set the "Device" menu item for the first time
731 self.update_item_device()
733 # Last folder used for saving episodes
734 self.folder_for_saving_episodes = None
736 # Set up default channel colors
737 self.channel_colors = {
738 'default': None,
739 'updating': gl.config.color_updating_feeds,
740 'parse_error': '#ff0000',
743 # Now, update the feed cache, when everything's in place
744 self.btnUpdateFeeds.show()
745 self.updated_feeds = 0
746 self.updating_feed_cache = False
747 self.feed_cache_update_cancelled = False
748 self.update_feed_cache(force_update=gl.config.update_on_startup)
750 # Clean up old, orphaned download files
751 partial_files = gl.find_partial_files()
753 # Message area
754 self.message_area = None
756 resumable_episodes = []
757 if len(partial_files) > 0:
758 for f in partial_files:
759 correct_name = os.path.basename(f)[:-len('.partial')] # strip ".partial"
760 log('Searching episode for file: %s', correct_name, sender=self)
761 found_episode = False
762 for c in self.channels:
763 for e in c.get_all_episodes():
764 if e.filename == correct_name:
765 log('Found episode: %s', e.title, sender=self)
766 resumable_episodes.append(e)
767 found_episode = True
768 if found_episode:
769 break
770 if found_episode:
771 break
773 if len(resumable_episodes):
774 self.download_episode_list_paused(resumable_episodes)
775 self.message_area = widgets.SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
776 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
777 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
778 self.message_area.show_all()
779 self.wNotebook.set_current_page(1)
781 gl.clean_up_downloads(delete_partial=False)
782 else:
783 gl.clean_up_downloads(delete_partial=True)
785 # Start the auto-update procedure
786 self.auto_update_procedure(first_run=True)
788 # Delete old episodes if the user wishes to
789 if gl.config.auto_remove_old_episodes:
790 old_episodes = self.get_old_episodes()
791 if len(old_episodes) > 0:
792 self.delete_episode_list(old_episodes, confirm=False)
793 self.updateComboBox()
795 # First-time users should be asked if they want to see the OPML
796 if len(self.channels) == 0:
797 util.idle_add(self.on_itemUpdate_activate, None)
799 def on_tool_downloads_toggled(self, toolbutton):
800 if toolbutton.get_active():
801 self.wNotebook.set_current_page(1)
802 else:
803 self.wNotebook.set_current_page(0)
805 def update_downloads_list(self):
806 model = self.treeDownloads.get_model()
808 downloading, failed, finished, queued, others = 0, 0, 0, 0, 0
809 total_speed, total_size, done_size = 0, 0, 0
811 # Keep a list of all download tasks that we've seen
812 download_tasks_seen = set()
814 # Remember the progress and speed for the episode that
815 # has been opened in the episode shownotes dialog (if any)
816 if self.gpodder_episode_window is not None:
817 episode_window_episode = self.gpodder_episode_window.episode
818 episode_window_progress = 0.0
819 episode_window_speed = 0.0
820 else:
821 episode_window_episode = None
823 for row in model:
824 self.download_status_manager.request_update(row.iter)
826 task = row[self.download_status_manager.C_TASK]
827 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
829 total_size += size
830 done_size += size*progress
832 if episode_window_episode is not None and \
833 episode_window_episode.url == task.url:
834 episode_window_progress = progress
835 episode_window_speed = speed
837 download_tasks_seen.add(task)
839 if status == download.DownloadTask.DOWNLOADING:
840 downloading += 1
841 total_speed += speed
842 elif status == download.DownloadTask.FAILED:
843 failed += 1
844 elif status == download.DownloadTask.DONE:
845 finished += 1
846 elif status == download.DownloadTask.QUEUED:
847 queued += 1
848 else:
849 others += 1
851 # Remember which tasks we have seen after this run
852 self.download_tasks_seen = download_tasks_seen
854 text = [_('Downloads')]
855 if downloading + failed + finished + queued > 0:
856 s = []
857 if downloading > 0:
858 s.append(_('%d downloading') % downloading)
859 if failed > 0:
860 s.append(_('%d failed') % failed)
861 if finished > 0:
862 s.append(_('%d done') % finished)
863 if queued > 0:
864 s.append(_('%d queued') % queued)
865 text.append(' (' + ', '.join(s)+')')
866 self.labelDownloads.set_text(''.join(text))
868 if gpodder.interface == gpodder.MAEMO:
869 sum = downloading + failed + finished + queued + others
870 self.tool_downloads.set_is_important(sum > 0)
871 if sum:
872 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
873 else:
874 self.tool_downloads.set_label(_('Downloads'))
876 title = [self.default_title]
878 # We have to update all episodes/channels for which the status has
879 # changed. Accessing task.status_changed has the side effect of
880 # re-setting the changed flag, so we need to get the "changed" list
881 # of tuples first and split it into two lists afterwards
882 changed = [(task.url, task.podcast_url) for task in \
883 self.download_tasks_seen if task.status_changed]
884 episode_urls = [episode_url for episode_url, channel_url in changed]
885 channel_urls = [channel_url for episode_url, channel_url in changed]
887 count = downloading + queued
888 if count > 0:
889 if count == 1:
890 title.append( _('downloading one file'))
891 elif count > 1:
892 title.append( _('downloading %d files') % count)
894 if total_size > 0:
895 percentage = 100.0*done_size/total_size
896 else:
897 percentage = 0.0
898 total_speed = gl.format_filesize(total_speed)
899 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
900 if self.tray_icon is not None:
901 # Update the tray icon status and progress bar
902 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
903 self.tray_icon.draw_progress_bar(percentage/100.)
904 elif self.last_download_count > 0:
905 if self.tray_icon is not None:
906 # Update the tray icon status
907 self.tray_icon.set_status()
908 self.tray_icon.downloads_finished(self.download_tasks_seen)
909 if gpodder.interface == gpodder.MAEMO:
910 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
911 log('All downloads have finished.', sender=self)
912 if gl.config.cmd_all_downloads_complete:
913 util.run_external_command(gl.config.cmd_all_downloads_complete)
914 self.last_download_count = count
916 self.gPodder.set_title(' - '.join(title))
918 self.update_episode_list_icons(episode_urls)
919 if self.gpodder_episode_window is not None and \
920 self.gpodder_episode_window.gPodderEpisode.get_property('visible'):
921 self.gpodder_episode_window.download_status_changed(episode_urls)
922 self.gpodder_episode_window.download_status_progress(episode_window_progress, episode_window_speed)
923 self.play_or_download()
924 if channel_urls:
925 self.updateComboBox(only_these_urls=channel_urls)
926 return True
928 def on_tree_channels_resize(self, widget, allocation):
929 if not gl.config.podcast_sidebar_save_space:
930 return
932 window_allocation = self.gPodder.get_allocation()
933 percentage = 100. * float(allocation.width) / float(window_allocation.width)
934 if hasattr(self, 'cell_channel_icon'):
935 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
936 if hasattr(self, 'cell_channel_pill'):
937 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
939 def entry_add_channel_focus(self, widget, event):
940 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
941 if widget.get_text() == self.ENTER_URL_TEXT:
942 widget.set_text('')
944 def entry_add_channel_unfocus(self, widget, event):
945 if widget.get_text() == '':
946 widget.set_text(self.ENTER_URL_TEXT)
947 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
949 def on_config_changed(self, name, old_value, new_value):
950 if name == 'show_toolbar' and gpodder.interface != gpodder.MAEMO:
951 if new_value:
952 self.toolbar.show()
953 else:
954 self.toolbar.hide()
955 elif name == 'episode_list_descriptions' and gpodder.interface != gpodder.MAEMO:
956 self.updateTreeView()
957 elif name == 'show_url_entry_in_podcast_list':
958 if new_value:
959 self.hboxAddChannel.show()
960 else:
961 self.hboxAddChannel.hide()
963 def read_apps(self):
964 time.sleep(3) # give other parts of gpodder a chance to start up
965 self.user_apps_reader.read()
966 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
967 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
969 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
970 # With get_bin_window, we get the window that contains the rows without
971 # the header. The Y coordinate of this window will be the height of the
972 # treeview header. This is the amount we have to subtract from the
973 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
974 (x_bin, y_bin) = treeview.get_bin_window().get_position()
975 y -= x_bin
976 y -= y_bin
977 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
979 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
980 self.last_tooltip_episode = None
981 return False
983 if path is not None:
984 model = treeview.get_model()
985 iter = model.get_iter(path)
986 url = model.get_value(iter, 0)
987 description = model.get_value(iter, 7) # FIXME INDEX MODEL BY SYMBOLIC NAME
988 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
989 self.last_tooltip_episode = None
990 return False
991 self.last_tooltip_episode = url
993 if len(description) > 400:
994 description = description[:398]+'[...]'
996 tooltip.set_text(description)
997 return True
999 self.last_tooltip_episode = None
1000 return False
1002 def podcast_list_allow_tooltips(self):
1003 self.podcast_list_can_tooltip = True
1005 def episode_list_allow_tooltips(self):
1006 self.episode_list_can_tooltip = True
1008 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1009 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1011 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
1012 self.last_tooltip_channel = None
1013 return False
1015 if path is not None:
1016 model = treeview.get_model()
1017 iter = model.get_iter(path)
1018 url = model.get_value(iter, 0)
1019 for channel in self.channels:
1020 if channel.url == url:
1021 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
1022 self.last_tooltip_channel = None
1023 return False
1024 self.last_tooltip_channel = channel
1025 channel.request_save_dir_size()
1026 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
1027 error_str = model.get_value(iter, 6)
1028 if error_str:
1029 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1030 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1031 table = gtk.Table(rows=3, columns=3)
1032 table.set_row_spacings(5)
1033 table.set_col_spacings(5)
1034 table.set_border_width(5)
1036 heading = gtk.Label()
1037 heading.set_alignment(0, 1)
1038 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1039 table.attach(heading, 0, 1, 0, 1)
1040 size_info = gtk.Label()
1041 size_info.set_alignment(1, 1)
1042 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1043 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1044 table.attach(size_info, 2, 3, 0, 1)
1046 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1048 if len(channel.description) < 500:
1049 description = channel.description
1050 else:
1051 pos = channel.description.find('\n\n')
1052 if pos == -1 or pos > 500:
1053 description = channel.description[:498]+'[...]'
1054 else:
1055 description = channel.description[:pos]
1057 description = gtk.Label(description)
1058 if error_str:
1059 description.set_markup(error_str)
1060 description.set_alignment(0, 0)
1061 description.set_line_wrap(True)
1062 table.attach(description, 0, 3, 2, 3)
1064 table.show_all()
1065 tooltip.set_custom(table)
1067 return True
1069 self.last_tooltip_channel = None
1070 return False
1072 def update_m3u_playlist_clicked(self, widget):
1073 self.active_channel.update_m3u_playlist()
1074 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
1076 def treeview_downloads_button_pressed(self, treeview, event):
1077 if event.button == 1:
1078 # Catch left mouse button presses, and if we there is no
1079 # path at the given position, deselect all items
1080 (x, y) = (int(event.x), int(event.y))
1081 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1082 if path is None:
1083 treeview.get_selection().unselect_all()
1085 # Use right-click for the Desktop version and left-click for Maemo
1086 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1087 (event.button == 3 and gpodder.interface == gpodder.GUI):
1088 (x, y) = (int(event.x), int(event.y))
1089 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
1091 paths = []
1092 # Did the user right-click into a selection?
1093 selection = treeview.get_selection()
1094 if selection.count_selected_rows() and path:
1095 (model, paths) = selection.get_selected_rows()
1096 if path not in paths:
1097 # We have right-clicked, but not into the
1098 # selection, assume we don't want to operate
1099 # on the selection
1100 paths = []
1102 # No selection or right click not in selection:
1103 # Select the single item where we clicked
1104 if not paths and path:
1105 treeview.grab_focus()
1106 treeview.set_cursor( path, column, 0)
1107 (model, paths) = (treeview.get_model(), [path])
1109 # We did not find a selection, and the user didn't
1110 # click on an item to select -- don't show the menu
1111 if not paths:
1112 return True
1114 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
1116 def make_menu_item(label, stock_id, tasks, status):
1117 # This creates a menu item for selection-wide actions
1118 def for_each_task_set_status(tasks, status):
1119 changed_episode_urls = []
1120 for row_reference, task in tasks:
1121 if status is not None:
1122 if status == download.DownloadTask.QUEUED:
1123 # Only queue task when its paused/failed/cancelled
1124 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1125 self.download_queue_manager.add_task(task)
1126 elif status == download.DownloadTask.CANCELLED:
1127 # Cancelling a download only allows when paused/downloading/queued
1128 if task.status in (task.QUEUED, task.DOWNLOADING, task.PAUSED):
1129 task.status = status
1130 elif status == download.DownloadTask.PAUSED:
1131 # Pausing a download only when queued/downloading
1132 if task.status in (task.DOWNLOADING, task.QUEUED):
1133 task.status = status
1134 else:
1135 # We (hopefully) can simply set the task status here
1136 task.status = status
1137 else:
1138 # Remove the selected task - cancel downloading/queued tasks
1139 if task.status in (task.QUEUED, task.DOWNLOADING):
1140 task.status = task.CANCELLED
1141 model.remove(model.get_iter(row_reference.get_path()))
1142 # Remember the URL, so we can tell the UI to update
1143 try:
1144 # We don't "see" this task anymore - remove it;
1145 # this is needed, so update_episode_list_icons()
1146 # below gets the correct list of "seen" tasks
1147 self.download_tasks_seen.remove(task)
1148 except KeyError, key_error:
1149 log('Cannot remove task from "seen" list: %s', task, sender=self)
1150 changed_episode_urls.append(task.url)
1151 # Tell the task that it has been removed (so it can clean up)
1152 task.removed_from_list()
1153 # Tell the podcasts tab to update icons for our removed podcasts
1154 self.update_episode_list_icons(changed_episode_urls)
1155 return True
1156 item = gtk.ImageMenuItem(label)
1157 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1158 item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
1160 # Determine if we should disable this menu item
1161 for row_reference, task in tasks:
1162 if status == download.DownloadTask.QUEUED:
1163 if task.status not in (download.DownloadTask.PAUSED, \
1164 download.DownloadTask.FAILED, \
1165 download.DownloadTask.CANCELLED):
1166 item.set_sensitive(False)
1167 break
1168 elif status == download.DownloadTask.CANCELLED:
1169 if task.status not in (download.DownloadTask.PAUSED, \
1170 download.DownloadTask.QUEUED, \
1171 download.DownloadTask.DOWNLOADING):
1172 item.set_sensitive(False)
1173 break
1174 elif status == download.DownloadTask.PAUSED:
1175 if task.status not in (download.DownloadTask.QUEUED, \
1176 download.DownloadTask.DOWNLOADING):
1177 item.set_sensitive(False)
1178 break
1179 elif status is None:
1180 if task.status not in (download.DownloadTask.CANCELLED, \
1181 download.DownloadTask.FAILED, \
1182 download.DownloadTask.DONE):
1183 item.set_sensitive(False)
1184 break
1186 return self.set_finger_friendly(item)
1188 menu = gtk.Menu()
1190 item = gtk.ImageMenuItem(_('Episode details'))
1191 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1192 if len(selected_tasks) == 1:
1193 row_reference, task = selected_tasks[0]
1194 episode = task.episode
1195 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1196 else:
1197 item.set_sensitive(False)
1198 menu.append(item)
1199 menu.append(gtk.SeparatorMenuItem())
1200 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
1201 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
1202 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
1203 menu.append(gtk.SeparatorMenuItem())
1204 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
1206 if gpodder.interface == gpodder.MAEMO:
1207 # Because we open the popup on left-click for Maemo,
1208 # we also include a non-action to close the menu
1209 menu.append(gtk.SeparatorMenuItem())
1210 item = gtk.ImageMenuItem(_('Close this menu'))
1211 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1212 menu.append(self.set_finger_friendly(item))
1214 menu.show_all()
1215 menu.popup(None, None, None, event.button, event.time)
1216 return True
1219 def treeview_channels_button_pressed( self, treeview, event):
1220 global WEB_BROWSER_ICON
1222 if gpodder.interface == gpodder.MAEMO:
1223 self.treeview_channels_buttonpress = (event.x, event.y)
1224 return True
1226 if event.button == 3:
1227 ( x, y ) = ( int(event.x), int(event.y) )
1228 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1230 paths = []
1232 # Did the user right-click into a selection?
1233 selection = treeview.get_selection()
1234 if selection.count_selected_rows() and path:
1235 ( model, paths ) = selection.get_selected_rows()
1236 if path not in paths:
1237 # We have right-clicked, but not into the
1238 # selection, assume we don't want to operate
1239 # on the selection
1240 paths = []
1242 # No selection or right click not in selection:
1243 # Select the single item where we clicked
1244 if not len( paths) and path:
1245 treeview.grab_focus()
1246 treeview.set_cursor( path, column, 0)
1248 ( model, paths ) = ( treeview.get_model(), [ path ] )
1250 # We did not find a selection, and the user didn't
1251 # click on an item to select -- don't show the menu
1252 if not len( paths):
1253 return True
1255 menu = gtk.Menu()
1257 item = gtk.ImageMenuItem( _('Open download folder'))
1258 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
1259 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1260 menu.append( item)
1262 item = gtk.ImageMenuItem( _('Update Feed'))
1263 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
1264 item.connect('activate', self.on_itemUpdateChannel_activate )
1265 item.set_sensitive( not self.updating_feed_cache )
1266 menu.append( item)
1268 if gl.config.create_m3u_playlists:
1269 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1270 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1271 item.connect('activate', self.update_m3u_playlist_clicked)
1272 menu.append(item)
1274 if self.active_channel.link:
1275 item = gtk.ImageMenuItem(_('Visit website'))
1276 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1277 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1278 menu.append(item)
1280 if self.active_channel.channel_is_locked:
1281 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1282 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1283 item.connect('activate', self.on_channel_toggle_lock_activate)
1284 menu.append(self.set_finger_friendly(item))
1285 else:
1286 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1287 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1288 item.connect('activate', self.on_channel_toggle_lock_activate)
1289 menu.append(self.set_finger_friendly(item))
1292 menu.append( gtk.SeparatorMenuItem())
1294 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1295 item.connect( 'activate', self.on_itemEditChannel_activate)
1296 menu.append( item)
1298 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1299 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1300 menu.append( item)
1302 menu.show_all()
1303 # Disable tooltips while we are showing the menu, so
1304 # the tooltip will not appear over the menu
1305 self.podcast_list_can_tooltip = False
1306 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
1307 menu.popup( None, None, None, event.button, event.time)
1309 return True
1311 def on_itemClose_activate(self, widget):
1312 if self.tray_icon is not None:
1313 if gpodder.interface == gpodder.MAEMO:
1314 self.gPodder.set_property('visible', False)
1315 else:
1316 self.iconify_main_window()
1317 else:
1318 self.on_gPodder_delete_event(widget)
1320 def cover_file_removed(self, channel_url):
1322 The Cover Downloader calls this when a previously-
1323 available cover has been removed from the disk. We
1324 have to update our cache to reflect this change.
1326 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
1327 for row in self.treeChannels.get_model():
1328 if row[COLUMN_URL] == channel_url:
1329 row[COLUMN_PIXBUF] = None
1330 key = (channel_url, gl.config.podcast_list_icon_size, \
1331 gl.config.podcast_list_icon_size)
1332 if key in self.cover_cache:
1333 del self.cover_cache[key]
1336 def cover_download_finished(self, channel_url, pixbuf):
1338 The Cover Downloader calls this when it has finished
1339 downloading (or registering, if already downloaded)
1340 a new channel cover, which is ready for displaying.
1342 if pixbuf is not None:
1343 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
1344 model = self.treeChannels.get_model()
1345 if model is None:
1346 # Not yet ready (race condition) - simply ignore
1347 return
1349 for row in model:
1350 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
1351 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)
1352 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
1354 def save_episode_as_file( self, url, *args):
1355 episode = self.active_channel.find_episode(url)
1357 if episode.was_downloaded(and_exists=True):
1358 folder = self.folder_for_saving_episodes
1359 copy_from = episode.local_filename(create=False)
1360 assert copy_from is not None
1361 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=episode.sync_filename(), dst_directory=folder)
1362 self.folder_for_saving_episodes = folder
1364 def copy_episode_bluetooth(self, url, *args):
1365 episode = self.active_channel.find_episode(url)
1367 if not episode.was_downloaded(and_exists=True):
1368 log('Cannot copy episode via bluetooth (does not exist!)', sender=self)
1370 filename = episode.local_filename(create=False)
1371 assert filename is not None
1373 if gl.config.bluetooth_use_device_address:
1374 device = gl.config.bluetooth_device_address
1375 else:
1376 device = None
1378 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
1379 (base, ext) = os.path.splitext(filename)
1380 if not destfile.endswith(ext):
1381 destfile += ext
1383 if gl.config.bluetooth_use_converter:
1384 title = _('Converting file')
1385 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
1386 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1387 dlg.set_title(title)
1388 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1389 dlg.show_all()
1390 else:
1391 dlg = None
1393 def convert_and_send_thread(filename, destfile, device, dialog, notify):
1394 if gl.config.bluetooth_use_converter:
1395 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
1396 result = p.wait()
1397 if dialog is not None:
1398 dialog.destroy()
1399 else:
1400 try:
1401 shutil.copyfile(filename, destfile)
1402 result = 0
1403 except:
1404 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1405 result = 1
1407 if result == 0 or not os.path.exists(destfile):
1408 util.bluetooth_send_file(destfile, device)
1409 else:
1410 notify(_('Error converting file.'), _('Bluetooth file transfer'))
1411 util.delete_file(destfile)
1413 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
1415 def treeview_button_savepos(self, treeview, event):
1416 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1417 self.treeview_available_buttonpress = (event.x, event.y)
1418 return True
1420 def treeview_channels_button_released(self, treeview, event):
1421 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1422 selection = self.treeChannels.get_selection()
1423 pathatpos = self.treeChannels.get_path_at_pos(int(event.x), int(event.y))
1424 if self.currently_updating:
1425 log('do not handle press while updating', sender=self)
1426 return True
1427 if pathatpos is None:
1428 return False
1429 else:
1430 ydistance = int(abs(event.y-self.treeview_channels_buttonpress[1]))
1431 xdistance = int(event.x-self.treeview_channels_buttonpress[0])
1432 if ydistance < 30:
1433 (path, column, x, y) = pathatpos
1434 selection.select_path(path)
1435 self.treeChannels.set_cursor(path)
1436 self.treeChannels.grab_focus()
1437 return True
1439 def treeview_button_pressed( self, treeview, event):
1440 global WEB_BROWSER_ICON
1442 if gpodder.interface == gpodder.MAEMO:
1443 ydistance = int(abs(event.y-self.treeview_available_buttonpress[1]))
1444 xdistance = int(event.x-self.treeview_available_buttonpress[0])
1446 selection = self.treeAvailable.get_selection()
1447 pathatpos = self.treeAvailable.get_path_at_pos(int(event.x), int(event.y))
1448 if pathatpos is None:
1449 # No item at the current cursor position
1450 return False
1451 elif ydistance < 30:
1452 # Item under the cursor, and no scrolling done
1453 (path, column, x, y) = pathatpos
1454 selection.select_path(path)
1455 self.treeAvailable.set_cursor(path)
1456 self.treeAvailable.grab_focus()
1457 if gl.config.maemo_enable_gestures and xdistance > 70:
1458 self.on_treeAvailable_row_activated(self.itemPlaySelected)
1459 return True
1460 elif gl.config.maemo_enable_gestures and xdistance < -70:
1461 self.on_treeAvailable_row_activated(self.treeAvailable)
1462 return True
1463 else:
1464 # Scrolling has been done
1465 return True
1467 # Use right-click for the Desktop version and left-click for Maemo
1468 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1469 (event.button == 3 and gpodder.interface == gpodder.GUI):
1470 ( x, y ) = ( int(event.x), int(event.y) )
1471 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1473 paths = []
1475 # Did the user right-click into a selection?
1476 selection = self.treeAvailable.get_selection()
1477 if selection.count_selected_rows() and path:
1478 ( model, paths ) = selection.get_selected_rows()
1479 if path not in paths:
1480 # We have right-clicked, but not into the
1481 # selection, assume we don't want to operate
1482 # on the selection
1483 paths = []
1485 # No selection or right click not in selection:
1486 # Select the single item where we clicked
1487 if not len( paths) and path:
1488 treeview.grab_focus()
1489 treeview.set_cursor( path, column, 0)
1491 ( model, paths ) = ( treeview.get_model(), [ path ] )
1493 # We did not find a selection, and the user didn't
1494 # click on an item to select -- don't show the menu
1495 if not len( paths):
1496 return True
1498 first_url = model.get_value( model.get_iter( paths[0]), 0)
1499 episode = db.load_episode(first_url)
1501 menu = gtk.Menu()
1503 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1505 if open_instead_of_play:
1506 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1507 else:
1508 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1510 item.set_sensitive(can_play)
1511 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolPlay))
1512 menu.append(self.set_finger_friendly(item))
1514 if not can_cancel:
1515 item = gtk.ImageMenuItem(_('Download'))
1516 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1517 item.set_sensitive(can_download)
1518 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolDownload))
1519 menu.append(self.set_finger_friendly(item))
1520 else:
1521 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1522 item.connect('activate', lambda w: self.on_treeDownloads_row_activated(self.toolCancel))
1523 menu.append(self.set_finger_friendly(item))
1525 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1526 item.set_sensitive(can_delete and not episode['is_locked'])
1527 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1528 menu.append(self.set_finger_friendly(item))
1530 # FIXME - fix the following block
1531 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1532 item = gtk.ImageMenuItem(_('Do not download'))
1533 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1534 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1535 menu.append(self.set_finger_friendly(item))
1536 elif episode['state'] == db.STATE_NORMAL and can_download:
1537 item = gtk.ImageMenuItem(_('Mark as new'))
1538 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1539 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1540 menu.append(self.set_finger_friendly(item))
1542 # Ok, this probably makes sense to only display for downloaded files
1543 if can_play and not can_download:
1544 menu.append( gtk.SeparatorMenuItem())
1545 item = gtk.ImageMenuItem(_('Save to disk'))
1546 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1547 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1548 menu.append(self.set_finger_friendly(item))
1549 if gl.bluetooth_available:
1550 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1551 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1552 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1553 menu.append(self.set_finger_friendly(item))
1554 if can_transfer:
1555 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1556 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1557 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1558 menu.append(self.set_finger_friendly(item))
1560 if can_play:
1561 menu.append( gtk.SeparatorMenuItem())
1562 is_played = episode['is_played']
1563 if is_played:
1564 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1565 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1566 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1567 menu.append(self.set_finger_friendly(item))
1568 else:
1569 item = gtk.ImageMenuItem(_('Mark as played'))
1570 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1571 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1572 menu.append(self.set_finger_friendly(item))
1574 is_locked = episode['is_locked']
1575 if is_locked:
1576 item = gtk.ImageMenuItem(_('Allow deletion'))
1577 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1578 item.connect('activate', self.on_item_toggle_lock_activate)
1579 menu.append(self.set_finger_friendly(item))
1580 else:
1581 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1582 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1583 item.connect('activate', self.on_item_toggle_lock_activate)
1584 menu.append(self.set_finger_friendly(item))
1586 menu.append(gtk.SeparatorMenuItem())
1587 # Single item, add episode information menu item
1588 episode_url = model.get_value(model.get_iter(paths[0]), 0)
1589 item = gtk.ImageMenuItem(_('Episode details'))
1590 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1591 item.set_sensitive(len(paths) == 1)
1592 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1593 menu.append(self.set_finger_friendly(item))
1595 episode = self.active_channel.find_episode(episode_url)
1596 # If we have it, also add episode website link
1597 if len(paths) == 1 and episode and episode.link and episode.link != episode.url:
1598 item = gtk.ImageMenuItem(_('Visit website'))
1599 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1600 item.connect('activate', lambda w: util.open_website(episode.link))
1601 menu.append(self.set_finger_friendly(item))
1603 if gpodder.interface == gpodder.MAEMO:
1604 # Because we open the popup on left-click for Maemo,
1605 # we also include a non-action to close the menu
1606 menu.append(gtk.SeparatorMenuItem())
1607 item = gtk.ImageMenuItem(_('Close this menu'))
1608 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1609 menu.append(self.set_finger_friendly(item))
1611 menu.show_all()
1612 # Disable tooltips while we are showing the menu, so
1613 # the tooltip will not appear over the menu
1614 self.episode_list_can_tooltip = False
1615 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1616 menu.popup( None, None, None, event.button, event.time)
1618 return True
1620 def set_title(self, new_title):
1621 self.default_title = new_title
1622 self.gPodder.set_title(new_title)
1624 def update_selected_episode_list_icons(self):
1626 Updates the status icons in the episode list
1628 selection = self.treeAvailable.get_selection()
1629 (model, paths) = selection.get_selected_rows()
1630 for path in paths:
1631 iter = model.get_iter(path)
1632 self.active_channel.iter_set_downloading_columns(model, iter, downloading=self.episode_is_downloading)
1634 def update_episode_list_icons(self, urls):
1636 Updates the status icons in the episode list
1637 Only update the episodes that have an URL in
1638 the "urls" iterable object (e.g. a list of URLs)
1640 if self.active_channel is None or not urls:
1641 return
1643 model = self.treeAvailable.get_model()
1644 if model is None:
1645 return
1647 for url in urls:
1648 if url in self.url_path_mapping:
1649 path = (self.url_path_mapping[url],)
1650 self.active_channel.iter_set_downloading_columns(model, model.get_iter(path), downloading=self.episode_is_downloading)
1652 def playback_episode(self, episode, stream=False):
1653 if gpodder.interface == gpodder.MAEMO:
1654 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Opening %s') % saxutils.escape(episode.title))
1655 def destroy_banner_later(banner):
1656 banner.destroy()
1657 return False
1658 gobject.timeout_add(5000, destroy_banner_later, banner)
1659 (success, application) = gl.playback_episode(episode, stream)
1660 if not success:
1661 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), ))
1662 self.update_selected_episode_list_icons()
1663 self.updateComboBox(only_selected_channel=True)
1665 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1666 if model is None:
1667 return True
1669 key = key.lower()
1671 # columns, as defined in libpodcasts' get model method
1672 # 1 = episode title, 7 = description
1673 columns = (1, 7)
1675 for column in columns:
1676 value = model.get_value( iter, column).lower()
1677 if value.find( key) != -1:
1678 return False
1680 return True
1682 def change_menu_item(self, menuitem, icon=None, label=None):
1683 if icon is not None:
1684 menuitem.set_property('stock-id', icon)
1685 if label is not None:
1686 menuitem.label = label
1688 def play_or_download(self):
1689 if self.wNotebook.get_current_page() > 0:
1690 return
1692 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1693 ( is_played, is_locked ) = (False,)*2
1695 open_instead_of_play = False
1697 selection = self.treeAvailable.get_selection()
1698 if selection.count_selected_rows() > 0:
1699 (model, paths) = selection.get_selected_rows()
1701 for path in paths:
1702 url = model.get_value( model.get_iter( path), 0)
1704 episode = self.active_channel.find_episode(url)
1706 if episode.file_type() not in ('audio', 'video'):
1707 open_instead_of_play = True
1709 if episode.was_downloaded():
1710 can_play = episode.was_downloaded(and_exists=True)
1711 can_delete = True
1712 is_played = episode.is_played
1713 is_locked = episode.is_locked
1714 if not can_play:
1715 can_download = True
1716 else:
1717 if self.episode_is_downloading(episode):
1718 can_cancel = True
1719 else:
1720 can_download = True
1722 can_download = can_download and not can_cancel
1723 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1724 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1726 if open_instead_of_play:
1727 if gpodder.interface != gpodder.MAEMO:
1728 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1729 can_transfer = False
1730 else:
1731 if gpodder.interface != gpodder.MAEMO:
1732 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1734 self.toolPlay.set_sensitive( can_play)
1735 self.toolDownload.set_sensitive( can_download)
1736 self.toolTransfer.set_sensitive( can_transfer)
1737 self.toolCancel.set_sensitive( can_cancel)
1739 self.item_cancel_download.set_sensitive(can_cancel)
1740 self.itemDownloadSelected.set_sensitive(can_download)
1741 self.itemOpenSelected.set_sensitive(can_play)
1742 self.itemPlaySelected.set_sensitive(can_play)
1743 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1744 self.item_toggle_played.set_sensitive(can_play)
1745 self.item_toggle_lock.set_sensitive(can_play)
1747 self.itemOpenSelected.set_visible(open_instead_of_play)
1748 self.itemPlaySelected.set_visible(not open_instead_of_play)
1750 if can_play:
1751 if is_played:
1752 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1753 else:
1754 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1755 if is_locked:
1756 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1757 else:
1758 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1760 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1762 def on_cbMaxDownloads_toggled(self, widget, *args):
1763 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1765 def on_cbLimitDownloads_toggled(self, widget, *args):
1766 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1768 def episode_new_status_changed(self, urls):
1769 self.updateComboBox()
1770 self.update_episode_list_icons(urls)
1772 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1773 selection = self.treeChannels.get_selection()
1774 (model, iter) = selection.get_selected()
1776 if only_selected_channel:
1777 # very cheap! only update selected channel
1778 if iter and self.active_channel is not None:
1779 update_channel_model_by_iter(model, iter,
1780 self.active_channel, self.channel_colors,
1781 self.cover_cache,
1782 gl.config.podcast_list_icon_size,
1783 gl.config.podcast_list_icon_size)
1784 elif not self.channel_list_changed:
1785 # we can keep the model, but have to update some
1786 if only_these_urls is None:
1787 # still cheaper than reloading the whole list
1788 iter = model.get_iter_first()
1789 while iter is not None:
1790 (index,) = model.get_path(iter)
1791 update_channel_model_by_iter(model, iter,
1792 self.channels[index], self.channel_colors,
1793 self.cover_cache,
1794 gl.config.podcast_list_icon_size,
1795 gl.config.podcast_list_icon_size)
1796 iter = model.iter_next(iter)
1797 else:
1798 # ok, we got a bunch of urls to update
1799 for url in only_these_urls:
1800 if url in self.channel_url_path_mapping:
1801 index = self.channel_url_path_mapping[url]
1802 path = (index,)
1803 iter = model.get_iter(path)
1804 update_channel_model_by_iter(model, iter,
1805 self.channels[index], self.channel_colors,
1806 self.cover_cache,
1807 gl.config.podcast_list_icon_size,
1808 gl.config.podcast_list_icon_size)
1809 else:
1810 if model and iter and selected_url is None:
1811 # Get the URL of the currently-selected podcast
1812 selected_url = model.get_value(iter, 0)
1814 (model, urls) = channels_to_model(self.channels,
1815 self.channel_colors, self.cover_cache,
1816 gl.config.podcast_list_icon_size,
1817 gl.config.podcast_list_icon_size)
1819 self.channel_url_path_mapping = dict(zip(urls, range(len(urls))))
1820 self.treeChannels.set_model(model)
1822 try:
1823 selected_path = (0,)
1824 # Find the previously-selected URL in the new
1825 # model if we have an URL (else select first)
1826 if selected_url is not None:
1827 pos = model.get_iter_first()
1828 while pos is not None:
1829 url = model.get_value(pos, 0)
1830 if url == selected_url:
1831 selected_path = model.get_path(pos)
1832 break
1833 pos = model.iter_next(pos)
1835 self.treeChannels.get_selection().select_path(selected_path)
1836 except:
1837 log( 'Cannot set selection on treeChannels', sender = self)
1838 self.on_treeChannels_cursor_changed( self.treeChannels)
1839 self.channel_list_changed = False
1841 def episode_is_downloading(self, episode):
1842 """Returns True if the given episode is being downloaded at the moment"""
1843 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1845 def updateTreeView(self):
1846 if self.channels and self.active_channel is not None:
1847 if gpodder.interface == gpodder.MAEMO:
1848 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes for %s') % saxutils.escape(self.active_channel.title))
1849 else:
1850 banner = None
1851 def thread_func(self, banner, active_channel):
1852 (model, urls) = self.active_channel.get_tree_model(self.episode_is_downloading)
1853 mapping = dict(zip(urls, range(len(urls))))
1854 def update_gui_with_new_model(self, channel, model, urls, mapping, banner):
1855 if self.active_channel is not None and channel is not None:
1856 log('%s <=> %s', self.active_channel.title, channel.title, sender=self)
1857 if self.active_channel == channel:
1858 self.treeAvailable.set_model(model)
1859 self.url_path_mapping = mapping
1860 self.treeAvailable.columns_autosize()
1861 self.play_or_download()
1862 if banner is not None:
1863 banner.destroy()
1864 self.currently_updating = False
1865 return False
1866 gobject.idle_add(lambda: update_gui_with_new_model(self, active_channel, model, urls, mapping, banner))
1867 self.currently_updating = True
1868 Thread(target=thread_func, args=[self, banner, self.active_channel]).start()
1869 else:
1870 model = self.treeAvailable.get_model()
1871 if model is not None:
1872 model.clear()
1874 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1875 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1877 dnd_channel = None
1878 if path is not None:
1879 model = self.treeChannels.get_model()
1880 iter = model.get_iter(path)
1881 url = model.get_value(iter, 0)
1882 for channel in self.channels:
1883 if channel.url == url:
1884 dnd_channel = channel
1885 break
1887 result = sel.data
1888 rl = result.strip().lower()
1889 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1890 services.cover_downloader.replace_cover(dnd_channel, result)
1891 else:
1892 self.add_new_channel(result)
1894 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1895 result = util.normalize_feed_url(result)
1896 (scheme, rest) = result.split('://', 1)
1898 if not result:
1899 cute_scheme = saxutils.escape(scheme)+'://'
1900 title = _('%s URLs are not supported') % cute_scheme
1901 message = _('gPodder does not understand the URL you supplied.')
1902 self.show_message( message, title)
1903 return
1905 for old_channel in self.channels:
1906 if old_channel.url == result:
1907 log( 'Channel already exists: %s', result)
1908 # Select the existing channel in combo box
1909 for i in range( len( self.channels)):
1910 if self.channels[i] == old_channel:
1911 self.treeChannels.get_selection().select_path( (i,))
1912 self.on_treeChannels_cursor_changed(self.treeChannels)
1913 break
1914 self.show_message( _('You have already subscribed to this podcast: %s') % (
1915 saxutils.escape( old_channel.title), ), _('Already added'))
1916 return
1918 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1919 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1920 waitdlg.set_title(_('Downloading episode list'))
1921 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1922 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1923 waitpb = gtk.ProgressBar()
1924 if block:
1925 waitdlg.vbox.add(waitpb)
1926 waitdlg.show_all()
1927 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1929 self.entryAddChannel.set_text(_('Downloading feed...'))
1930 self.entryAddChannel.set_sensitive(False)
1931 self.btnAddChannel.set_sensitive(False)
1932 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1933 thread = Thread( target=self.add_new_channel_proc, args=args )
1934 thread.start()
1936 while block and thread.isAlive():
1937 while gtk.events_pending():
1938 gtk.main_iteration( False)
1939 waitpb.pulse()
1940 time.sleep(0.1)
1943 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1944 log( 'Adding new channel: %s', url)
1945 channel = error = None
1946 try:
1947 channel = PodcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1948 except HTTPAuthError, e:
1949 error = e
1950 except Exception, e:
1951 log('Error in PodcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1953 util.idle_add( callback, channel, url, error, *callback_args )
1955 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1956 if channel is not None:
1957 self.channels.append( channel)
1958 self.channel_list_changed = True
1959 save_channels( self.channels)
1960 if not quiet:
1961 # download changed channels and select the new episode in the UI afterwards
1962 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1964 try:
1965 (username, password) = util.username_password_from_url(url)
1966 except ValueError, ve:
1967 self.show_message(_('The following error occured while trying to get authentication data from the URL:') + '\n\n' + ve.message, _('Error getting authentication data'))
1968 (username, password) = (None, None)
1969 log('Error getting authentication data from URL: %s', url, traceback=True)
1971 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')):
1972 channel.username = username
1973 channel.password = password
1974 log('Saving authentication data for episode downloads..', sender = self)
1975 channel.save()
1976 # We need to update the channel list otherwise the authentication
1977 # data won't show up in the channel editor.
1978 # TODO: Only updated the newly added feed to save some cpu cycles
1979 self.channels = load_channels()
1980 self.channel_list_changed = True
1982 if ask_download_new:
1983 new_episodes = channel.get_new_episodes(downloading=self.episode_is_downloading)
1984 if len(new_episodes):
1985 self.new_episodes_show(new_episodes)
1987 elif isinstance( error, HTTPAuthError ):
1988 response, auth_tokens = self.UsernamePasswordDialog(
1989 _('Feed requires authentication'), _('Please enter your username and password.'))
1991 if response:
1992 self.add_new_channel( url, authentication_tokens=auth_tokens )
1994 else:
1995 # Ok, the URL is not a channel, or there is some other
1996 # error - let's see if it's a web page or OPML file...
1997 try:
1998 data = urllib2.urlopen(url).read().lower()
1999 if '</opml>' in data:
2000 # This looks like an OPML feed
2001 self.on_item_import_from_file_activate(None, url)
2003 elif '</html>' in data:
2004 # This looks like a web page
2005 title = _('The URL is a website')
2006 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.)')
2007 if self.show_confirmation(message, title):
2008 util.open_website(url)
2010 except Exception, e:
2011 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
2013 title = _('Error adding podcast')
2014 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
2015 self.show_message( message, title)
2017 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
2018 self.entryAddChannel.set_sensitive(True)
2019 self.btnAddChannel.set_sensitive(True)
2020 self.update_podcasts_tab()
2021 waitdlg.destroy()
2024 def update_feed_cache_finish_callback(self, channels=None,
2025 notify_no_new_episodes=False, select_url_afterwards=None):
2027 db.commit()
2029 self.updating_feed_cache = False
2030 if gpodder.interface == gpodder.MAEMO:
2031 self.btnCancelFeedUpdate.show()
2032 self.itemUpdate.set_sensitive(True)
2033 self.itemUpdateChannel.set_sensitive(True)
2035 # If we want to select a specific podcast (via its URL)
2036 # after the update, we give it to updateComboBox here to
2037 # select exactly this podcast after updating the view
2038 self.updateComboBox(selected_url=select_url_afterwards)
2040 self.channels = load_channels()
2041 self.channel_list_changed = True
2042 self.updateComboBox()
2044 episodes = self.get_new_episodes()
2046 if self.tray_icon:
2047 self.tray_icon.set_status(None)
2048 if self.minimized:
2049 # Determine new episodes that we have not yet announced
2050 new_episodes = [episode for episode in episodes \
2051 if episode not in self.already_notified_new_episodes]
2052 self.already_notified_new_episodes.extend(new_episodes)
2054 if len(new_episodes) == 0:
2055 if notify_no_new_episodes and self.tray_icon is not None:
2056 msg = _('No new episodes available for download')
2057 self.tray_icon.send_notification(msg)
2058 else:
2059 if len(new_episodes) == 1:
2060 title = _('gPodder has found %s') % (_('one new episode:'),)
2061 else:
2062 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
2063 message = self.tray_icon.format_episode_list([e.title for e in new_episodes])
2065 #auto download new episodes
2066 if gl.config.auto_download_when_minimized:
2067 message += '\n<i>(%s...)</i>' % _('downloading')
2068 self.download_episode_list(new_episodes)
2069 self.tray_icon.send_notification(message, title)
2071 if len(episodes) == 0 or self.feed_cache_update_cancelled:
2072 self.pbFeedUpdate.set_fraction(1.0)
2073 if self.feed_cache_update_cancelled:
2074 self.pbFeedUpdate.set_text(_('Update has been cancelled'))
2075 else:
2076 self.pbFeedUpdate.set_text(_('No new episodes'))
2077 self.feed_cache_update_cancelled = True
2078 self.btnCancelFeedUpdate.show()
2079 self.btnCancelFeedUpdate.set_sensitive(True)
2080 if gpodder.interface == gpodder.MAEMO:
2081 # btnCancelFeedUpdate is a ToolButton on Maemo
2082 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2083 else:
2084 # btnCancelFeedUpdate is a normal gtk.Button
2085 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2086 else:
2087 if self.minimized and gl.config.auto_download_when_minimized:
2088 new_episodes = [episode for episode in episodes if episode not in self.already_notified_new_episodes]
2089 self.already_notified_new_episodes.extend(new_episodes)
2090 if len(new_episodes) > 0:
2091 self.download_episode_list(new_episodes)
2092 else:
2093 # open the episodes selection dialog
2094 self.new_episodes_show(episodes)
2096 def update_feed_cache_callback(self, progressbar, title, position, count):
2097 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
2098 progressbar.set_text(progression)
2099 if self.tray_icon:
2100 self.tray_icon.set_status(
2101 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
2102 if count > 0:
2103 progressbar.set_fraction(float(position)/float(count))
2105 def update_feed_cache_proc( self, channel, total_channels, semaphore,
2106 callback_proc, finish_proc):
2108 semaphore.acquire()
2109 if not self.feed_cache_update_cancelled:
2110 try:
2111 channel.update()
2112 except:
2113 log('Darn SQLite LOCK!', sender=self, traceback=True)
2115 # By the time we get here the update may have already been cancelled
2116 if not self.feed_cache_update_cancelled:
2117 callback_proc(channel.title, self.updated_feeds, total_channels)
2119 self.updated_feeds += 1
2120 self.treeview_channel_set_color( channel, 'default' )
2121 channel.update_flag = False
2123 semaphore.release()
2124 if self.updated_feeds == total_channels:
2125 finish_proc()
2127 def on_btnCancelFeedUpdate_clicked(self, widget):
2128 if self.feed_cache_update_cancelled:
2129 if gpodder.interface == gpodder.MAEMO:
2130 self.btnUpdateSelectedFeed.show()
2131 self.toolFeedUpdateProgress.hide()
2132 self.btnCancelFeedUpdate.hide()
2133 self.btnCancelFeedUpdate.set_is_important(False)
2134 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2135 self.toolbarSpacer.set_expand(True)
2136 self.toolbarSpacer.set_draw(False)
2137 else:
2138 self.hboxUpdateFeeds.hide()
2139 self.btnUpdateFeeds.show()
2140 else:
2141 self.pbFeedUpdate.set_text(_('Cancelling, please wait...'))
2142 self.feed_cache_update_cancelled = True
2143 self.btnCancelFeedUpdate.set_sensitive(False)
2145 def update_feed_cache(self, channels=None, force_update=True,
2146 notify_no_new_episodes=False, select_url_afterwards=None):
2148 if self.updating_feed_cache:
2149 return
2151 if not force_update:
2152 self.channels = load_channels()
2153 self.channel_list_changed = True
2154 self.updateComboBox()
2155 return
2157 self.updating_feed_cache = True
2158 self.itemUpdate.set_sensitive(False)
2159 self.itemUpdateChannel.set_sensitive(False)
2161 if self.tray_icon:
2162 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2164 if channels is None:
2165 channels = self.channels
2167 if len(channels) == 1:
2168 text = _('Updating "%s"...') % channels[0].title
2169 else:
2170 text = _('Updating %d feeds...') % len(channels)
2171 self.pbFeedUpdate.set_text(text)
2172 self.pbFeedUpdate.set_fraction(0)
2174 # let's get down to business..
2175 callback_proc = lambda title, pos, count: util.idle_add(
2176 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
2177 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
2178 channels, notify_no_new_episodes, select_url_afterwards )
2180 self.updated_feeds = 0
2181 self.feed_cache_update_cancelled = False
2182 self.btnCancelFeedUpdate.show()
2183 self.btnCancelFeedUpdate.set_sensitive(True)
2184 if gpodder.interface == gpodder.MAEMO:
2185 self.toolbarSpacer.set_expand(False)
2186 self.toolbarSpacer.set_draw(True)
2187 self.btnUpdateSelectedFeed.hide()
2188 self.toolFeedUpdateProgress.show_all()
2189 else:
2190 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2191 self.hboxUpdateFeeds.show_all()
2192 self.btnUpdateFeeds.hide()
2193 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
2195 for channel in channels:
2196 self.treeview_channel_set_color( channel, 'updating' )
2197 channel.update_flag = True
2198 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
2199 thread = Thread( target = self.update_feed_cache_proc, args = args)
2200 thread.start()
2202 def treeview_channel_set_color( self, channel, color ):
2203 if self.treeChannels.get_model():
2204 if color in self.channel_colors:
2205 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
2206 else:
2207 self.treeChannels.get_model().set(channel.iter, 8, color)
2209 def on_gPodder_delete_event(self, widget, *args):
2210 """Called when the GUI wants to close the window
2211 Displays a confirmation dialog (and closes/hides gPodder)
2214 downloading = self.download_status_manager.are_downloads_in_progress()
2216 # Only iconify if we are using the window's "X" button,
2217 # but not when we are using "Quit" in the menu or toolbar
2218 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
2219 self.iconify_main_window()
2220 elif gl.config.on_quit_ask or downloading:
2221 if gpodder.interface == gpodder.MAEMO:
2222 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2223 if result:
2224 self.close_gpodder()
2225 else:
2226 return True
2227 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2228 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2229 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2231 title = _('Quit gPodder')
2232 if downloading:
2233 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2234 else:
2235 message = _('Do you really want to quit gPodder now?')
2237 dialog.set_title(title)
2238 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2239 if not downloading:
2240 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2241 dialog.vbox.pack_start(cb_ask)
2242 cb_ask.show_all()
2244 result = dialog.run()
2245 dialog.destroy()
2247 if result == gtk.RESPONSE_CLOSE:
2248 if not downloading and cb_ask.get_active() == True:
2249 gl.config.on_quit_ask = False
2250 self.close_gpodder()
2251 else:
2252 self.close_gpodder()
2254 return True
2256 def close_gpodder(self):
2257 """ clean everything and exit properly
2259 if self.channels:
2260 if save_channels(self.channels):
2261 if gl.config.my_gpodder_autoupload:
2262 log('Uploading to my.gpodder.org on close', sender=self)
2263 util.idle_add(self.on_upload_to_mygpo, None)
2264 else:
2265 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
2267 self.gPodder.hide()
2269 if self.tray_icon is not None:
2270 self.tray_icon.set_visible(False)
2272 # Notify all tasks to to carry out any clean-up actions
2273 self.download_status_manager.tell_all_tasks_to_quit()
2275 while gtk.events_pending():
2276 gtk.main_iteration(False)
2278 db.close()
2280 self.quit()
2281 sys.exit(0)
2283 def get_old_episodes(self):
2284 episodes = []
2285 for channel in self.channels:
2286 for episode in channel.get_downloaded_episodes():
2287 if episode.is_old() and not episode.is_locked and episode.is_played:
2288 episodes.append(episode)
2289 return episodes
2291 def for_each_selected_episode_url( self, callback):
2292 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
2293 for path in paths:
2294 url = model.get_value( model.get_iter( path), 0)
2295 try:
2296 callback( url)
2297 except Exception, e:
2298 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
2300 self.update_selected_episode_list_icons()
2301 self.updateComboBox(only_selected_channel=True)
2302 db.commit()
2304 def delete_episode_list( self, episodes, confirm = True):
2305 if len(episodes) == 0:
2306 return
2308 if len(episodes) == 1:
2309 message = _('Do you really want to delete this episode?')
2310 else:
2311 message = _('Do you really want to delete %d episodes?') % len(episodes)
2313 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
2314 return
2316 episode_urls = set()
2317 channel_urls = set()
2318 for episode in episodes:
2319 log('Deleting episode: %s', episode.title, sender = self)
2320 episode.delete_from_disk()
2321 episode_urls.add(episode.url)
2322 channel_urls.add(episode.channel.url)
2324 # Episodes have been deleted - persist the database
2325 db.commit()
2327 #self.download_status_updated(episode_urls, channel_urls)
2329 def on_itemRemoveOldEpisodes_activate( self, widget):
2330 columns = (
2331 ('title_and_description', None, None, _('Episode')),
2332 ('channel_prop', None, None, _('Podcast')),
2333 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2334 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2335 ('played_prop', None, None, _('Status')),
2336 ('age_prop', None, None, _('Downloaded')),
2339 selection_buttons = {
2340 _('Select played'): lambda episode: episode.is_played,
2341 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
2344 instructions = _('Select the episodes you want to delete from your hard disk.')
2346 episodes = []
2347 selected = []
2348 for channel in self.channels:
2349 for episode in channel.get_downloaded_episodes():
2350 if not episode.is_locked:
2351 episodes.append(episode)
2352 selected.append(episode.is_played)
2354 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
2355 episodes = episodes, selected = selected, columns = columns, \
2356 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2357 selection_buttons = selection_buttons)
2359 def mark_selected_episodes_new(self):
2360 callback = lambda url: self.active_channel.find_episode(url).mark_new()
2361 self.for_each_selected_episode_url(callback)
2363 def mark_selected_episodes_old(self):
2364 callback = lambda url: self.active_channel.find_episode(url).mark_old()
2365 self.for_each_selected_episode_url(callback)
2367 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2368 if toggle:
2369 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
2370 else:
2371 callback = lambda url: db.mark_episode(url, is_played=new_value)
2373 self.for_each_selected_episode_url(callback)
2375 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2376 if toggle:
2377 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
2378 else:
2379 callback = lambda url: db.mark_episode(url, is_locked=new_value)
2381 self.for_each_selected_episode_url(callback)
2383 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2384 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2385 db.update_channel_lock(self.active_channel)
2387 if self.active_channel.channel_is_locked:
2388 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2389 else:
2390 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2392 for episode in self.active_channel.get_all_episodes():
2393 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
2395 self.updateComboBox(only_selected_channel=True)
2397 def on_item_email_subscriptions_activate(self, widget):
2398 if not self.channels:
2399 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
2400 elif not gl.send_subscriptions():
2401 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
2403 def on_itemUpdateChannel_activate(self, widget=None):
2404 self.update_feed_cache(channels=[self.active_channel,])
2406 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
2407 restore_from = can_restore_from_opml()
2409 if self.channels:
2410 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
2411 elif restore_from is not None:
2412 title = _('Database upgrade required')
2413 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?')
2414 if self.show_confirmation(message, title):
2415 add_callback = lambda url: self.add_new_channel(url, False, True)
2416 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
2417 w.set_has_separator(False)
2418 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
2419 w.set_default_size(500, -1)
2420 pb = gtk.ProgressBar()
2421 l = gtk.Label()
2422 l.set_padding(6, 3)
2423 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
2424 l.set_alignment(0.0, 0.5)
2425 w.vbox.pack_start(l)
2426 l = gtk.Label()
2427 l.set_padding(6, 3)
2428 l.set_alignment(0.0, 0.5)
2429 l.set_text(_('Please wait while your settings are converted.'))
2430 w.vbox.pack_start(l)
2431 w.vbox.pack_start(pb)
2432 lb = gtk.Label()
2433 lb.set_ellipsize(pango.ELLIPSIZE_END)
2434 lb.set_alignment(0.0, 0.5)
2435 lb.set_padding(6, 6)
2436 w.vbox.pack_start(lb)
2438 def set_pb_status(pb, lb, fraction, text):
2439 pb.set_fraction(float(fraction)/100.0)
2440 pb.set_text('%.0f %%' % fraction)
2441 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
2442 while gtk.events_pending():
2443 gtk.main_iteration(False)
2444 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
2445 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
2446 w.show_all()
2447 start = datetime.datetime.now()
2448 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
2449 # Refresh the view with the updated episodes
2450 self.updateComboBox()
2451 time_taken = str(datetime.datetime.now()-start)
2452 status_callback(100.0, _('Migration finished in %s') % time_taken)
2453 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
2454 w.run()
2455 w.destroy()
2456 else:
2457 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
2459 def download_episode_list_paused(self, episodes):
2460 self.download_episode_list(episodes, True)
2462 def download_episode_list(self, episodes, add_paused=False):
2463 for episode in episodes:
2464 log('Downloading episode: %s', episode.title, sender = self)
2465 if not episode.was_downloaded(and_exists=True):
2466 task_exists = False
2467 for task in self.download_tasks_seen:
2468 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2469 self.download_queue_manager.add_task(task)
2470 task_exists = True
2471 continue
2473 if task_exists:
2474 continue
2476 task = download.DownloadTask(episode)
2477 if add_paused:
2478 task.status = task.PAUSED
2479 self.download_queue_manager.add_resumed_task(task)
2480 else:
2481 self.download_queue_manager.add_task(task)
2483 def new_episodes_show(self, episodes):
2484 columns = (
2485 ('title_and_description', None, None, _('Episode')),
2486 ('channel_prop', None, None, _('Podcast')),
2487 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2488 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2491 instructions = _('Select the episodes you want to download now.')
2493 self.feed_cache_update_cancelled = True
2494 self.on_btnCancelFeedUpdate_clicked(self.btnCancelFeedUpdate)
2496 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
2497 episodes=episodes, columns=columns, selected_default=True, \
2498 stock_ok_button = 'gpodder-download', \
2499 callback=self.download_episode_list, \
2500 remove_callback=lambda e: e.mark_old(), \
2501 remove_action=_('Never download'), \
2502 remove_finished=self.episode_new_status_changed)
2504 def on_itemDownloadAllNew_activate(self, widget, *args):
2505 new_episodes = self.get_new_episodes()
2506 if len(new_episodes):
2507 self.new_episodes_show(new_episodes)
2508 else:
2509 msg = _('No new episodes available for download')
2510 if self.tray_icon is not None and self.minimized:
2511 self.tray_icon.send_notification(msg)
2512 else:
2513 self.show_message(msg, _('No new episodes'))
2515 def get_new_episodes(self, channels=None):
2516 if channels is None:
2517 channels = self.channels
2518 episodes = []
2519 for channel in channels:
2520 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2521 episodes.append(episode)
2523 return episodes
2525 def get_all_episodes(self, exclude_nonsignificant=True ):
2526 """'exclude_nonsignificant' will exclude non-downloaded episodes
2527 and all episodes from channels that are set to skip when syncing"""
2528 episode_list = []
2529 for channel in self.channels:
2530 if not channel.sync_to_devices and exclude_nonsignificant:
2531 log('Skipping channel: %s', channel.title, sender=self)
2532 continue
2533 for episode in channel.get_all_episodes():
2534 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
2535 episode_list.append(episode)
2536 return episode_list
2538 def ipod_delete_played(self, device):
2539 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
2540 episodes_on_device = device.get_all_tracks()
2541 for local_episode in all_episodes:
2542 device_episode = device.episode_on_device(local_episode)
2543 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2544 or local_episode.state == db.STATE_DELETED ):
2545 log("mp3_player_delete_played: removing %s" % device_episode.title)
2546 device.remove_track(device_episode)
2548 def on_sync_to_ipod_activate(self, widget, episodes=None):
2549 # make sure gpod is available before even trying to sync
2550 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2551 title = _('Cannot Sync To iPod')
2552 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2553 self.notification( message, title )
2554 return
2555 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2556 title = _('Cannot sync to MTP device')
2557 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2558 self.notification( message, title )
2559 return
2561 device = sync.open_device()
2562 device.register( 'post-done', self.sync_to_ipod_completed )
2564 if device is None:
2565 title = _('No device configured')
2566 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2567 self.notification(message, title)
2568 return
2570 if not device.open():
2571 title = _('Cannot open device')
2572 message = _('There has been an error opening your device.')
2573 self.notification(message, title)
2574 return
2576 if gl.config.device_type == 'ipod':
2577 #update played episodes and delete if requested
2578 for channel in self.channels:
2579 if channel.sync_to_devices:
2580 allepisodes = [ episode for episode in channel.get_all_episodes() if episode.was_downloaded(and_exists=True) ]
2581 device.update_played_or_delete(channel, allepisodes, gl.config.ipod_delete_played_from_db)
2583 if gl.config.ipod_purge_old_episodes:
2584 device.purge()
2586 sync_all_episodes = not bool(episodes)
2588 if episodes is None:
2589 episodes = self.get_all_episodes()
2591 # make sure we have enough space on the device
2592 total_size = 0
2593 free_space = device.get_free_space()
2594 for episode in episodes:
2595 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
2596 filename = episode.local_filename(create=False)
2597 if filename is not None:
2598 total_size += util.calculate_size(str(filename))
2600 if total_size > free_space:
2601 # can be negative because of the 10 MiB for reserved for the iTunesDB
2602 free_space = max( free_space, 0 )
2603 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
2604 title = _('Not enough space left on device.')
2605 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
2606 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
2607 self.notification(message, title)
2608 device.close()
2609 else:
2610 # start syncing!
2611 gPodderSync(device=device, gPodder=self)
2612 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2613 if self.tray_icon:
2614 self.tray_icon.set_synchronisation_device(device)
2616 # The sync process might have updated the status of episodes,
2617 # therefore persist the database here to avoid losing data
2618 db.commit()
2620 def sync_to_ipod_completed(self, device, successful_sync):
2621 device.unregister( 'post-done', self.sync_to_ipod_completed )
2623 if self.tray_icon:
2624 self.tray_icon.release_synchronisation_device()
2626 if not successful_sync:
2627 title = _('Error closing device')
2628 message = _('There has been an error closing your device.')
2629 self.notification(message, title)
2631 # update model for played state updates after sync
2632 util.idle_add(self.updateComboBox)
2634 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2635 if sync_all_episodes:
2636 device.add_tracks(episodes)
2637 # 'only_sync_not_played' must be used or else all the played
2638 # tracks will be copied then immediately deleted
2639 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2640 self.ipod_delete_played(device)
2641 else:
2642 device.add_tracks(episodes, force_played=True)
2643 device.close()
2644 self.update_selected_episode_list_icons()
2646 def ipod_cleanup_callback(self, device, tracks):
2647 title = _('Delete podcasts from device?')
2648 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?')
2649 if len(tracks) > 0 and self.show_confirmation(message, title):
2650 gPodderSync(device=device, gPodder=self)
2651 Thread(target=self.ipod_cleanup_thread, args=[device, tracks]).start()
2653 def ipod_cleanup_thread(self, device, tracks):
2654 device.remove_tracks(tracks)
2656 if not device.close():
2657 title = _('Error closing device')
2658 message = _('There has been an error closing your device.')
2659 gobject.idle_add(self.show_message, message, title)
2661 def on_cleanup_ipod_activate(self, widget, *args):
2662 columns = (
2663 ('title', None, None, _('Episode')),
2664 ('podcast', None, None, _('Podcast')),
2665 ('filesize', None, None, _('Size')),
2666 ('modified', None, None, _('Copied')),
2667 ('playcount', None, None, _('Play count')),
2668 ('released', None, None, _('Released')),
2671 device = sync.open_device()
2673 if device is None:
2674 title = _('No device configured')
2675 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2676 self.show_message(message, title)
2677 return
2679 if not device.open():
2680 title = _('Cannot open device')
2681 message = _('There has been an error opening your device.')
2682 self.show_message(message, title)
2683 return
2685 tracks = device.get_all_tracks()
2686 if len(tracks) > 0:
2687 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2688 wanted_columns = []
2689 for key, sort_name, sort_type, caption in columns:
2690 want_this_column = False
2691 for track in tracks:
2692 if getattr(track, key) is not None:
2693 want_this_column = True
2694 break
2696 if want_this_column:
2697 wanted_columns.append((key, sort_name, sort_type, caption))
2698 title = _('Remove podcasts from device')
2699 instructions = _('Select the podcast episodes you want to remove from your device.')
2700 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2701 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2702 else:
2703 title = _('No files on device')
2704 message = _('The devices contains no files to be removed.')
2705 self.show_message(message, title)
2706 device.close()
2708 def on_manage_device_playlist(self, widget):
2709 # make sure gpod is available before even trying to sync
2710 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2711 title = _('Cannot manage iPod playlist')
2712 message = _('This feature is not available for iPods.')
2713 self.notification( message, title )
2714 return
2715 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2716 title = _('Cannot manage MTP device playlist')
2717 message = _('This feature is not available for MTP devices.')
2718 self.notification( message, title )
2719 return
2721 device = sync.open_device()
2723 if device is None:
2724 title = _('No device configured')
2725 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2726 self.notification(message, title)
2727 return
2729 if not device.open():
2730 title = _('Cannot open device')
2731 message = _('There has been an error opening your device.')
2732 self.notification(message, title)
2733 return
2735 gPodderPlaylist(device=device, gPodder=self)
2736 device.close()
2738 def show_hide_tray_icon(self):
2739 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2740 self.tray_icon = trayicon.GPodderStatusIcon(self, gpodder.icon_file)
2741 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2742 self.tray_icon.set_visible(False)
2743 del self.tray_icon
2744 self.tray_icon = None
2746 if gl.config.minimize_to_tray and self.tray_icon:
2747 self.tray_icon.set_visible(self.minimized)
2748 elif self.tray_icon:
2749 self.tray_icon.set_visible(True)
2751 def on_itemShowToolbar_activate(self, widget):
2752 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2754 def on_itemShowDescription_activate(self, widget):
2755 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2757 def update_item_device( self):
2758 if gl.config.device_type != 'none':
2759 self.itemDevice.set_visible(True)
2760 self.itemDevice.label = gl.get_device_name()
2761 else:
2762 self.itemDevice.set_visible(False)
2764 def properties_closed( self):
2765 self.show_hide_tray_icon()
2766 self.update_item_device()
2767 self.updateComboBox()
2769 def on_itemPreferences_activate(self, widget, *args):
2770 if gpodder.interface == gpodder.GUI:
2771 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2772 else:
2773 gPodderMaemoPreferences()
2775 def on_itemDependencies_activate(self, widget):
2776 gPodderDependencyManager()
2778 def on_add_new_google_search(self, widget, *args):
2779 def add_google_video_search(query):
2780 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2782 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2784 def on_upgrade_from_videocenter(self, widget):
2785 from gpodder import nokiavideocenter
2786 vc = nokiavideocenter.UpgradeFromVideocenter()
2787 if vc.db2opml():
2788 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))
2789 else:
2790 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2792 def require_my_gpodder_authentication(self):
2793 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2794 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'))
2795 if success and authentication[0] and authentication[1]:
2796 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2797 return True
2798 else:
2799 return False
2801 return True
2803 def my_gpodder_offer_autoupload(self):
2804 if not gl.config.my_gpodder_autoupload:
2805 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')):
2806 gl.config.my_gpodder_autoupload = True
2808 def on_download_from_mygpo(self, widget):
2809 if self.require_my_gpodder_authentication():
2810 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2811 opml_data = client.download_subscriptions()
2812 if len(opml_data) > 0:
2813 fp = open(gl.channel_opml_file, 'w')
2814 fp.write(opml_data)
2815 fp.close()
2816 (added, skipped) = (0, 0)
2817 i = opml.Importer(gl.channel_opml_file)
2818 for item in i.items:
2819 url = item['url']
2820 if url not in (c.url for c in self.channels):
2821 self.add_new_channel(url, ask_download_new=False, block=True)
2822 added += 1
2823 else:
2824 log('Already added: %s', url, sender=self)
2825 skipped += 1
2826 self.updateComboBox()
2827 if added > 0:
2828 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2829 elif widget is not None:
2830 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2831 self.my_gpodder_offer_autoupload()
2832 else:
2833 gl.config.my_gpodder_password = ''
2834 self.on_download_from_mygpo(widget)
2835 else:
2836 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2838 def on_upload_to_mygpo(self, widget):
2839 if self.require_my_gpodder_authentication():
2840 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2841 save_channels(self.channels)
2842 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2843 if widget is not None:
2844 self.show_message('\n'.join(messages), _('Results of upload'))
2845 if not success:
2846 gl.config.my_gpodder_password = ''
2847 self.on_upload_to_mygpo(widget)
2848 else:
2849 self.my_gpodder_offer_autoupload()
2850 elif not success:
2851 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2852 elif widget is not None:
2853 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2855 def on_itemAddChannel_activate(self, widget, *args):
2856 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2858 def on_itemEditChannel_activate(self, widget, *args):
2859 if self.active_channel is None:
2860 title = _('No podcast selected')
2861 message = _('Please select a podcast in the podcasts list to edit.')
2862 self.show_message( message, title)
2863 return
2865 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2867 def change_channel_url(self, channel, new_url):
2868 old_url = channel.url
2869 log('=> change channel url from %s to %s', old_url, new_url)
2870 channel.url = new_url
2871 # remove etag and last_modified to force an update
2872 channel.etag = ''
2873 channel.last_modified = ''
2874 (success, error) = channel.update()
2875 if not success:
2876 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2877 channel.url = old_url
2879 # Remove old episodes which haven't been downloaded.
2880 db.delete_empty_episodes(channel.id);
2882 # Update the OPML file.
2883 save_channels(self.channels)
2885 # update feed cache and select the podcast with the new URL afterwards
2886 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2888 def on_itemRemoveChannel_activate(self, widget, *args):
2889 try:
2890 if gpodder.interface == gpodder.GUI:
2891 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2892 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2893 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2895 title = _('Remove podcast and episodes?')
2896 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2898 dialog.set_title(title)
2899 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2901 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2902 dialog.vbox.pack_start(cb_ask)
2903 cb_ask.show_all()
2904 affirmative = gtk.RESPONSE_YES
2905 elif gpodder.interface == gpodder.MAEMO:
2906 cb_ask = gtk.CheckButton('') # dummy check button
2907 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2908 affirmative = gtk.RESPONSE_OK
2910 result = dialog.run()
2911 dialog.destroy()
2913 if result == affirmative:
2914 # delete downloaded episodes only if checkbox is unchecked
2915 if cb_ask.get_active() == False:
2916 self.active_channel.remove_downloaded()
2917 else:
2918 log('Not removing downloaded episodes', sender=self)
2920 # Clean up downloads and download directories
2921 gl.clean_up_downloads()
2923 # cancel any active downloads from this channel
2924 for episode in self.active_channel.get_all_episodes():
2925 self.download_status_manager.cancel_by_url(episode.url)
2927 # get the URL of the podcast we want to select next
2928 position = self.channels.index(self.active_channel)
2929 if position == len(self.channels)-1:
2930 # this is the last podcast, so select the URL
2931 # of the item before this one (i.e. the "new last")
2932 select_url = self.channels[position-1].url
2933 else:
2934 # there is a podcast after the deleted one, so
2935 # we simply select the one that comes after it
2936 select_url = self.channels[position+1].url
2938 # Remove the channel
2939 self.active_channel.delete()
2940 self.channels.remove(self.active_channel)
2941 self.channel_list_changed = True
2942 save_channels(self.channels)
2944 # Re-load the channels and select the desired new channel
2945 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2946 except:
2947 log('There has been an error removing the channel.', traceback=True, sender=self)
2948 self.update_podcasts_tab()
2950 def get_opml_filter(self):
2951 filter = gtk.FileFilter()
2952 filter.add_pattern('*.opml')
2953 filter.add_pattern('*.xml')
2954 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2955 return filter
2957 def on_item_import_from_file_activate(self, widget, filename=None):
2958 if filename is None:
2959 if gpodder.interface == gpodder.GUI:
2960 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2961 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2962 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2963 elif gpodder.interface == gpodder.MAEMO:
2964 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2965 dlg.set_filter(self.get_opml_filter())
2966 response = dlg.run()
2967 filename = None
2968 if response == gtk.RESPONSE_OK:
2969 filename = dlg.get_filename()
2970 dlg.destroy()
2972 if filename is not None:
2973 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))
2975 def on_itemExportChannels_activate(self, widget, *args):
2976 if not self.channels:
2977 title = _('Nothing to export')
2978 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2979 self.show_message( message, title)
2980 return
2982 if gpodder.interface == gpodder.GUI:
2983 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2984 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2985 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2986 elif gpodder.interface == gpodder.MAEMO:
2987 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2988 dlg.set_filter(self.get_opml_filter())
2989 response = dlg.run()
2990 if response == gtk.RESPONSE_OK:
2991 filename = dlg.get_filename()
2992 dlg.destroy()
2993 exporter = opml.Exporter( filename)
2994 if exporter.write(self.channels):
2995 if len(self.channels) == 1:
2996 title = _('One subscription exported')
2997 else:
2998 title = _('%d subscriptions exported') % len(self.channels)
2999 self.show_message(_('Your podcast list has been successfully exported.'), title)
3000 else:
3001 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
3002 else:
3003 dlg.destroy()
3005 def on_itemImportChannels_activate(self, widget, *args):
3006 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))
3008 def on_homepage_activate(self, widget, *args):
3009 util.open_website(gpodder.__url__)
3011 def on_wiki_activate(self, widget, *args):
3012 util.open_website('http://wiki.gpodder.org/')
3014 def on_bug_tracker_activate(self, widget, *args):
3015 util.open_website('http://bugs.gpodder.org/')
3017 def on_itemAbout_activate(self, widget, *args):
3018 dlg = gtk.AboutDialog()
3019 dlg.set_name('gPodder')
3020 dlg.set_version(gpodder.__version__)
3021 dlg.set_copyright(gpodder.__copyright__)
3022 dlg.set_website(gpodder.__url__)
3023 dlg.set_translator_credits( _('translator-credits'))
3024 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3026 if gpodder.interface == gpodder.GUI:
3027 # For the "GUI" version, we add some more
3028 # items to the about dialog (credits and logo)
3029 dlg.set_authors(app_authors)
3030 try:
3031 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3032 except:
3033 dlg.set_logo_icon_name('gpodder')
3035 dlg.run()
3037 def on_wNotebook_switch_page(self, widget, *args):
3038 page_num = args[1]
3039 if gpodder.interface == gpodder.MAEMO:
3040 self.tool_downloads.set_active(page_num == 1)
3041 page = self.wNotebook.get_nth_page(page_num)
3042 tab_label = self.wNotebook.get_tab_label(page).get_text()
3043 if page_num == 0 and self.active_channel is not None:
3044 self.set_title(self.active_channel.title)
3045 else:
3046 self.set_title(tab_label)
3047 if page_num == 0:
3048 self.play_or_download()
3049 self.menuChannels.set_sensitive(True)
3050 self.menuSubscriptions.set_sensitive(True)
3051 # The message area in the downloads tab should be hidden
3052 # when the user switches away from the downloads tab
3053 if self.message_area is not None:
3054 self.message_area.hide()
3055 self.message_area = None
3056 else:
3057 self.menuChannels.set_sensitive(False)
3058 self.menuSubscriptions.set_sensitive(False)
3059 self.toolDownload.set_sensitive( False)
3060 self.toolPlay.set_sensitive( False)
3061 self.toolTransfer.set_sensitive( False)
3062 self.toolCancel.set_sensitive( False)#services.download_status_manager.has_items())
3064 def on_treeChannels_row_activated(self, widget, path, *args):
3065 # double-click action of the podcast list or enter
3066 self.treeChannels.set_cursor(path)
3068 def on_treeChannels_cursor_changed(self, widget, *args):
3069 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3071 if model is not None and iter is not None:
3072 old_active_channel = self.active_channel
3073 (id,) = model.get_path(iter)
3074 self.active_channel = self.channels[id]
3076 if self.active_channel == old_active_channel:
3077 return
3079 if gpodder.interface == gpodder.MAEMO:
3080 self.set_title(self.active_channel.title)
3081 self.itemEditChannel.set_visible(True)
3082 self.itemRemoveChannel.set_visible(True)
3083 self.channel_toggle_lock.set_visible(True)
3084 if self.active_channel.channel_is_locked:
3085 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
3086 else:
3087 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
3089 else:
3090 self.active_channel = None
3091 self.itemEditChannel.set_visible(False)
3092 self.itemRemoveChannel.set_visible(False)
3093 self.channel_toggle_lock.set_visible(False)
3095 self.updateTreeView()
3097 def on_entryAddChannel_changed(self, widget, *args):
3098 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
3099 self.btnAddChannel.set_sensitive( active)
3101 def on_btnAddChannel_clicked(self, widget, *args):
3102 url = self.entryAddChannel.get_text()
3103 self.entryAddChannel.set_text('')
3104 self.add_new_channel( url)
3106 def on_btnEditChannel_clicked(self, widget, *args):
3107 self.on_itemEditChannel_activate( widget, args)
3109 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
3111 What this function does depends on from which widget it is called.
3112 It gets the selected episodes of the current podcast and runs one
3113 of the following actions on them:
3115 * Transfer (to MP3 player, iPod, etc..)
3116 * Playback/open files
3117 * Show the episode info dialog
3118 * Download episodes
3120 try:
3121 selection = self.treeAvailable.get_selection()
3122 (model, paths) = selection.get_selected_rows()
3124 if len(paths) == 0:
3125 log('Nothing selected', sender=self)
3126 return
3128 wname = widget.get_name()
3129 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
3130 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
3131 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
3133 episodes = []
3134 for path in paths:
3135 it = model.get_iter(path)
3136 url = model.get_value(it, 0)
3137 episode = self.active_channel.find_episode(url)
3138 episodes.append(episode)
3140 if len(episodes) == 0:
3141 log('No episodes selected', sender=self)
3143 if do_transfer:
3144 self.on_sync_to_ipod_activate(widget, episodes)
3145 elif do_playback:
3146 for episode in episodes:
3147 if episode.was_downloaded(and_exists=True):
3148 self.playback_episode(episode)
3149 elif gl.config.enable_streaming:
3150 self.playback_episode(episode, stream=True)
3151 elif do_epdialog:
3152 self.show_episode_shownotes(episode)
3153 else:
3154 self.download_episode_list(episodes)
3155 self.update_selected_episode_list_icons()
3156 self.play_or_download()
3157 except:
3158 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
3160 def show_episode_shownotes(self, episode):
3161 play_callback = lambda: self.playback_episode(episode)
3162 def download_callback():
3163 self.download_episode_list([episode])
3164 self.play_or_download()
3165 if self.gpodder_episode_window is None:
3166 log('First-time use of episode window --- creating', sender=self)
3167 self.gpodder_episode_window = gPodderEpisode(\
3168 download_status_manager=self.download_status_manager, \
3169 episode_is_downloading=self.episode_is_downloading)
3170 self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
3172 def on_treeAvailable_button_release_event(self, widget, *args):
3173 self.play_or_download()
3175 def auto_update_procedure(self, first_run=False):
3176 log('auto_update_procedure() got called', sender=self)
3177 if not first_run and gl.config.auto_update_feeds and self.minimized:
3178 self.update_feed_cache(force_update=True)
3180 next_update = 60*1000*gl.config.auto_update_frequency
3181 gobject.timeout_add(next_update, self.auto_update_procedure)
3183 def on_treeDownloads_row_activated(self, widget, *args):
3184 if self.wNotebook.get_current_page() == 0:
3185 # Use the available podcasts treeview + model
3186 selection = self.treeAvailable.get_selection()
3187 (model, paths) = selection.get_selected_rows()
3188 urls = [model.get_value(model.get_iter(path), 0) for path in paths]
3189 selected_tasks = [task for task in self.download_tasks_seen if task.url in urls]
3190 for task in selected_tasks:
3191 task.status = task.CANCELLED
3192 self.update_selected_episode_list_icons()
3193 self.play_or_download()
3194 return
3196 # Use the standard way of working on the treeview
3197 selection = self.treeDownloads.get_selection()
3198 (model, paths) = selection.get_selected_rows()
3199 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3201 for tree_row_reference, task in selected_tasks:
3202 if task.status in (task.DOWNLOADING, task.QUEUED):
3203 task.status = task.PAUSED
3204 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3205 self.download_queue_manager.add_task(task)
3206 elif task.status == task.DONE:
3207 model.remove(model.get_iter(tree_row_reference.get_path()))
3209 self.play_or_download()
3211 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
3212 self.on_treeDownloads_row_activated( widget, None)
3214 def on_btnCancelAll_clicked(self, widget, *args):
3215 self.treeDownloads.get_selection().select_all()
3216 self.on_treeDownloads_row_activated( self.toolCancel, None)
3217 self.treeDownloads.get_selection().unselect_all()
3219 def on_btnDownloadedDelete_clicked(self, widget, *args):
3220 if self.active_channel is None:
3221 return
3223 channel_url = self.active_channel.url
3224 selection = self.treeAvailable.get_selection()
3225 ( model, paths ) = selection.get_selected_rows()
3227 if selection.count_selected_rows() == 0:
3228 log( 'Nothing selected - will not remove any downloaded episode.')
3229 return
3231 if selection.count_selected_rows() == 1:
3232 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
3234 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
3235 if episode['is_locked']:
3236 title = _('%s is locked') % episode_title
3237 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
3238 self.notification(message, title)
3239 return
3241 title = _('Remove %s?') % episode_title
3242 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.")
3243 else:
3244 title = _('Remove %d episodes?') % selection.count_selected_rows()
3245 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.')
3247 locked_count = 0
3248 for path in paths:
3249 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
3250 if episode['is_locked']:
3251 locked_count += 1
3253 if selection.count_selected_rows() == locked_count:
3254 title = _('Episodes are locked')
3255 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3256 self.notification(message, title)
3257 return
3258 elif locked_count > 0:
3259 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
3260 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.')
3262 # if user confirms deletion, let's remove some stuff ;)
3263 if self.show_confirmation( message, title):
3264 try:
3265 # iterate over the selection, see also on_treeDownloads_row_activated
3266 for path in paths:
3267 url = model.get_value( model.get_iter( path), 0)
3268 self.active_channel.delete_episode_by_url( url)
3270 # now, clear local db cache so we can re-read it
3271 self.updateComboBox()
3272 except:
3273 log( 'Error while deleting (some) downloads.', traceback=True, sender=self)
3275 # only delete partial files if we do not have any downloads in progress
3276 delete_partial = False #not services.download_status_manager.has_items()
3277 gl.clean_up_downloads(delete_partial)
3278 self.update_selected_episode_list_icons()
3279 self.play_or_download()
3281 def on_key_press(self, widget, event):
3282 # Allow tab switching with Ctrl + PgUp/PgDown
3283 if event.state & gtk.gdk.CONTROL_MASK:
3284 if event.keyval == gtk.keysyms.Page_Up:
3285 self.wNotebook.prev_page()
3286 return True
3287 elif event.keyval == gtk.keysyms.Page_Down:
3288 self.wNotebook.next_page()
3289 return True
3291 # After this code we only handle Maemo hardware keys,
3292 # so if we are not a Maemo app, we don't do anything
3293 if gpodder.interface != gpodder.MAEMO:
3294 return False
3296 if event.keyval == gtk.keysyms.F6:
3297 if self.fullscreen:
3298 self.window.unfullscreen()
3299 else:
3300 self.window.fullscreen()
3301 if event.keyval == gtk.keysyms.Escape:
3302 new_visibility = not self.vboxChannelNavigator.get_property('visible')
3303 self.vboxChannelNavigator.set_property('visible', new_visibility)
3304 self.column_size.set_visible(not new_visibility)
3305 self.column_released.set_visible(not new_visibility)
3307 diff = 0
3308 if event.keyval == gtk.keysyms.F7: #plus
3309 diff = 1
3310 elif event.keyval == gtk.keysyms.F8: #minus
3311 diff = -1
3313 if diff != 0 and not self.currently_updating:
3314 selection = self.treeChannels.get_selection()
3315 (model, iter) = selection.get_selected()
3316 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3317 selection.select_path(new_path)
3318 self.treeChannels.set_cursor(new_path)
3319 return True
3321 return False
3323 def window_state_event(self, widget, event):
3324 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
3325 self.fullscreen = True
3326 else:
3327 self.fullscreen = False
3329 old_minimized = self.minimized
3331 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
3332 if gpodder.interface == gpodder.MAEMO:
3333 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
3335 if old_minimized != self.minimized and self.tray_icon:
3336 self.gPodder.set_skip_taskbar_hint(self.minimized)
3337 elif not self.tray_icon:
3338 self.gPodder.set_skip_taskbar_hint(False)
3340 if gl.config.minimize_to_tray and self.tray_icon:
3341 self.tray_icon.set_visible(self.minimized)
3343 def uniconify_main_window(self):
3344 if self.minimized:
3345 self.gPodder.present()
3347 def iconify_main_window(self):
3348 if not self.minimized:
3349 self.gPodder.iconify()
3351 def update_podcasts_tab(self):
3352 if len(self.channels):
3353 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3354 else:
3355 self.label2.set_text(_('Podcasts'))
3357 @dbus.service.method(gpodder.dbus_interface)
3358 def show_gui_window(self):
3359 self.gPodder.present()
3361 class gPodderChannel(BuilderWidget):
3362 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
3364 def new(self):
3365 global WEB_BROWSER_ICON
3366 self.changed = False
3367 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
3368 self.gPodderChannel.set_title( self.channel.title)
3369 self.entryTitle.set_text( self.channel.title)
3370 self.entryURL.set_text( self.channel.url)
3372 self.LabelDownloadTo.set_text( self.channel.save_dir)
3373 self.LabelWebsite.set_text( self.channel.link)
3375 self.cbNoSync.set_active( not self.channel.sync_to_devices)
3376 self.musicPlaylist.set_text(self.channel.device_playlist_name)
3377 if self.channel.username:
3378 self.FeedUsername.set_text( self.channel.username)
3379 if self.channel.password:
3380 self.FeedPassword.set_text( self.channel.password)
3382 services.cover_downloader.register('cover-available', self.cover_download_finished)
3383 services.cover_downloader.request_cover(self.channel)
3385 # Hide the website button if we don't have a valid URL
3386 if not self.channel.link:
3387 self.btn_website.hide_all()
3389 b = gtk.TextBuffer()
3390 b.set_text( self.channel.description)
3391 self.channel_description.set_buffer( b)
3393 #Add Drag and Drop Support
3394 flags = gtk.DEST_DEFAULT_ALL
3395 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
3396 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
3397 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
3398 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
3400 def on_btn_website_clicked(self, widget):
3401 util.open_website(self.channel.link)
3403 def on_btnDownloadCover_clicked(self, widget):
3404 if gpodder.interface == gpodder.GUI:
3405 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3406 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3407 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3408 elif gpodder.interface == gpodder.MAEMO:
3409 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
3411 if dlg.run() == gtk.RESPONSE_OK:
3412 url = dlg.get_uri()
3413 services.cover_downloader.replace_cover(self.channel, url)
3415 dlg.destroy()
3417 def on_btnClearCover_clicked(self, widget):
3418 services.cover_downloader.replace_cover(self.channel)
3420 def cover_download_finished(self, channel_url, pixbuf):
3421 if pixbuf is not None:
3422 self.imgCover.set_from_pixbuf(pixbuf)
3423 self.gPodderChannel.show()
3425 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
3426 files = sel.data.strip().split('\n')
3427 if len(files) != 1:
3428 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
3429 return
3431 file = files[0]
3433 if file.startswith('file://') or file.startswith('http://'):
3434 services.cover_downloader.replace_cover(self.channel, file)
3435 return
3437 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
3439 def on_gPodderChannel_destroy(self, widget, *args):
3440 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
3442 def on_btnOK_clicked(self, widget, *args):
3443 entered_url = self.entryURL.get_text()
3444 channel_url = self.channel.url
3446 if entered_url != channel_url:
3447 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
3448 if hasattr(self, 'callback_change_url'):
3449 self.gPodderChannel.hide_all()
3450 self.callback_change_url(self.channel, entered_url)
3452 self.channel.sync_to_devices = not self.cbNoSync.get_active()
3453 self.channel.device_playlist_name = self.musicPlaylist.get_text()
3454 self.channel.set_custom_title( self.entryTitle.get_text())
3455 self.channel.username = self.FeedUsername.get_text().strip()
3456 self.channel.password = self.FeedPassword.get_text()
3457 self.channel.save()
3459 self.gPodderChannel.destroy()
3460 self.callback_closed()
3462 class gPodderAddPodcastDialog(BuilderWidget):
3463 finger_friendly_widgets = ['btn_close', 'btn_add']
3465 def new(self):
3466 if not hasattr(self, 'url_callback'):
3467 log('No url callback set', sender=self)
3468 self.url_callback = None
3469 if hasattr(self, 'custom_label'):
3470 self.label_add.set_text(self.custom_label)
3471 if hasattr(self, 'custom_title'):
3472 self.gPodderAddPodcastDialog.set_title(self.custom_title)
3473 if gpodder.interface == gpodder.MAEMO:
3474 self.entry_url.set_text('http://')
3475 self.gPodderAddPodcastDialog.show()
3477 def on_btn_close_clicked(self, widget):
3478 self.gPodderAddPodcastDialog.destroy()
3480 def on_btn_paste_clicked(self, widget):
3481 clipboard = gtk.Clipboard()
3482 clipboard.request_text(self.receive_clipboard_text)
3484 def receive_clipboard_text(self, clipboard, text, data=None):
3485 if text is not None:
3486 self.entry_url.set_text(text)
3487 else:
3488 self.show_message(_('Nothing to paste.'), _('Clipboard is empty'))
3490 def on_entry_url_changed(self, widget):
3491 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
3493 def on_btn_add_clicked(self, widget):
3494 url = self.entry_url.get_text()
3495 self.on_btn_close_clicked(widget)
3496 if self.url_callback is not None:
3497 self.url_callback(url)
3500 class gPodderMaemoPreferences(BuilderWidget):
3501 finger_friendly_widgets = ['btn_close', 'btn_advanced']
3502 audio_players = [
3503 ('default', 'Media Player'),
3504 ('panucci', 'Panucci'),
3506 video_players = [
3507 ('default', 'Media Player'),
3508 ('mplayer', 'MPlayer'),
3511 def new(self):
3512 gl.config.connect_gtk_togglebutton('display_tray_icon', self.check_show_status_icon)
3513 gl.config.connect_gtk_togglebutton('on_quit_ask', self.check_ask_on_quit)
3514 gl.config.connect_gtk_togglebutton('maemo_enable_gestures', self.check_enable_gestures)
3516 for item in self.audio_players:
3517 command, caption = item
3518 if util.find_command(command) is None and command != 'default':
3519 self.audio_players.remove(item)
3521 for item in self.video_players:
3522 command, caption = item
3523 if util.find_command(command) is None and command != 'default':
3524 self.video_players.remove(item)
3526 # Set up the audio player combobox
3527 found = False
3528 self.userconfigured_player = None
3529 for id, audio_player in enumerate(self.audio_players):
3530 command, caption = audio_player
3531 self.combo_player_model.append([caption])
3532 if gl.config.player == command:
3533 self.combo_player.set_active(id)
3534 found = True
3535 if not found:
3536 self.combo_player_model.append(['User-configured (%s)' % gl.config.player])
3537 self.combo_player.set_active(len(self.combo_player_model)-1)
3538 self.userconfigured_player = gl.config.player
3540 # Set up the video player combobox
3541 found = False
3542 self.userconfigured_videoplayer = None
3543 for id, video_player in enumerate(self.video_players):
3544 command, caption = video_player
3545 self.combo_videoplayer_model.append([caption])
3546 if gl.config.videoplayer == command:
3547 self.combo_videoplayer.set_active(id)
3548 found = True
3549 if not found:
3550 self.combo_videoplayer_model.append(['User-configured (%s)' % gl.config.videoplayer])
3551 self.combo_videoplayer.set_active(len(self.combo_videoplayer_model)-1)
3552 self.userconfigured_videoplayer = gl.config.videoplayer
3554 self.gPodderMaemoPreferences.show()
3556 def on_combo_player_changed(self, combobox):
3557 index = combobox.get_active()
3558 if index < len(self.audio_players):
3559 gl.config.player = self.audio_players[index][0]
3560 elif self.userconfigured_player is not None:
3561 gl.config.player = self.userconfigured_player
3563 def on_combo_videoplayer_changed(self, combobox):
3564 index = combobox.get_active()
3565 if index < len(self.video_players):
3566 gl.config.videoplayer = self.video_players[index][0]
3567 elif self.userconfigured_videoplayer is not None:
3568 gl.config.videoplayer = self.userconfigured_videoplayer
3570 def on_btn_advanced_clicked(self, widget):
3571 self.gPodderMaemoPreferences.destroy()
3572 gPodderConfigEditor()
3574 def on_btn_close_clicked(self, widget):
3575 self.gPodderMaemoPreferences.destroy()
3578 class gPodderProperties(BuilderWidget):
3579 def new(self):
3580 if not hasattr( self, 'callback_finished'):
3581 self.callback_finished = None
3583 if gpodder.interface == gpodder.MAEMO:
3584 self.table5.hide_all() # player
3585 self.gPodderProperties.fullscreen()
3587 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
3588 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
3589 gl.config.connect_gtk_editable( 'player', self.openApp)
3590 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
3591 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
3592 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
3593 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
3594 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
3595 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
3596 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
3597 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
3598 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
3599 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
3600 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
3601 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
3602 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
3603 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
3604 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
3605 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
3606 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
3607 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
3608 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
3609 gl.config.connect_gtk_togglebutton('ipod_delete_played_from_db', self.ipod_delete_played_from_db)
3610 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
3611 gl.config.connect_gtk_togglebutton('disable_pre_sync_conversion', self.player_supports_ogg)
3613 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
3614 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
3616 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
3618 self.iPodMountpoint.set_label( gl.config.ipod_mount)
3619 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
3620 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
3622 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
3623 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
3625 if tagging_supported():
3626 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
3627 else:
3628 self.updatetags.set_sensitive( False)
3629 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
3630 self.updatetags.set_label( new_label)
3632 # device type
3633 self.comboboxDeviceType.set_active( 0)
3634 if gl.config.device_type == 'ipod':
3635 self.comboboxDeviceType.set_active( 1)
3636 elif gl.config.device_type == 'filesystem':
3637 self.comboboxDeviceType.set_active( 2)
3638 elif gl.config.device_type == 'mtp':
3639 self.comboboxDeviceType.set_active( 3)
3641 # setup cell renderers
3642 cellrenderer = gtk.CellRendererPixbuf()
3643 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
3644 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3645 cellrenderer = gtk.CellRendererText()
3646 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
3647 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3649 cellrenderer = gtk.CellRendererPixbuf()
3650 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
3651 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3652 cellrenderer = gtk.CellRendererText()
3653 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
3654 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3656 if not hasattr(self, 'user_apps_reader'):
3657 self.user_apps_reader = UserAppsReader(['audio', 'video'])
3659 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
3660 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
3662 if gpodder.interface == gpodder.GUI:
3663 self.user_apps_reader.read()
3665 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
3666 index = self.find_active_audio_app()
3667 self.comboAudioPlayerApp.set_active(index)
3668 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
3669 index = self.find_active_video_app()
3670 self.comboVideoPlayerApp.set_active(index)
3672 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
3674 def is_row_separator(self, model, iter):
3675 return model.get_value(iter, 0) == ''
3677 def update_mountpoint( self, ipod):
3678 if ipod is None or ipod.mount_point is None:
3679 self.iPodMountpoint.set_label( '')
3680 else:
3681 self.iPodMountpoint.set_label( ipod.mount_point)
3683 def find_active_audio_app(self):
3684 index_custom = -1
3685 model = self.comboAudioPlayerApp.get_model()
3686 iter = model.get_iter_first()
3687 index = 0
3688 while iter is not None:
3689 command = model.get_value(iter, 1)
3690 if command == self.openApp.get_text():
3691 return index
3692 if index_custom < 0 and command == '':
3693 index_custom = index
3694 iter = model.iter_next(iter)
3695 index += 1
3696 # return index of custom command or first item
3697 return max(0, index_custom)
3699 def find_active_video_app( self):
3700 index_custom = -1
3701 model = self.comboVideoPlayerApp.get_model()
3702 iter = model.get_iter_first()
3703 index = 0
3704 while iter is not None:
3705 command = model.get_value(iter, 1)
3706 if command == self.openVideoApp.get_text():
3707 return index
3708 if index_custom < 0 and command == '':
3709 index_custom = index
3710 iter = model.iter_next(iter)
3711 index += 1
3712 # return index of custom command or first item
3713 return max(0, index_custom)
3715 def set_download_dir( self, new_download_dir, event = None):
3716 gl.downloaddir = self.chooserDownloadTo.get_filename()
3717 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3718 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'))
3720 if event:
3721 event.set()
3723 def on_auto_update_feeds_toggled( self, widget, *args):
3724 self.auto_update_frequency.set_sensitive(widget.get_active())
3726 def on_display_tray_icon_toggled( self, widget, *args):
3727 self.enable_notifications.set_sensitive(widget.get_active())
3728 self.minimize_to_tray.set_sensitive(widget.get_active())
3730 def on_cbCustomSyncName_toggled( self, widget, *args):
3731 self.entryCustomSyncName.set_sensitive( widget.get_active())
3733 def on_only_sync_not_played_toggled( self, widget, *args):
3734 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3735 if not widget.get_active():
3736 self.delete_episodes_marked_played.set_active(False)
3738 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3739 if widget.get_active() and self.only_sync_not_played.get_active():
3740 self.on_sync_leave.set_active(True)
3741 self.on_sync_delete.set_sensitive(not widget.get_active())
3742 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3744 def on_btnCustomSyncNameHelp_clicked( self, widget):
3745 examples = [
3746 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3747 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3748 '<i>{episode.published}</i> -&gt; <b>20070908</b> (for 08.09.2007)',
3749 '<i>{episode.pubtime}</i> -&gt; <b>1344</b> (for 13:44)',
3750 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3753 info = [
3754 _('You can specify a custom format string for the file names on your MP3 player here.'),
3755 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3756 '\n'.join( [ ' %s' % s for s in examples ])
3759 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3761 def on_gPodderProperties_destroy(self, widget, *args):
3762 self.on_btnOK_clicked( widget, *args)
3764 def on_btnConfigEditor_clicked(self, widget, *args):
3765 self.on_btnOK_clicked(widget, *args)
3766 gPodderConfigEditor()
3768 def on_comboAudioPlayerApp_changed(self, widget, *args):
3769 # find out which one
3770 iter = self.comboAudioPlayerApp.get_active_iter()
3771 model = self.comboAudioPlayerApp.get_model()
3772 command = model.get_value( iter, 1)
3773 if command == '':
3774 if self.openApp.get_text() == 'default':
3775 self.openApp.set_text('')
3776 self.openApp.set_sensitive( True)
3777 self.openApp.show()
3778 self.labelCustomCommand.show()
3779 else:
3780 self.openApp.set_text( command)
3781 self.openApp.set_sensitive( False)
3782 self.openApp.hide()
3783 self.labelCustomCommand.hide()
3785 def on_comboVideoPlayerApp_changed(self, widget, *args):
3786 # find out which one
3787 iter = self.comboVideoPlayerApp.get_active_iter()
3788 model = self.comboVideoPlayerApp.get_model()
3789 command = model.get_value(iter, 1)
3790 if command == '':
3791 if self.openVideoApp.get_text() == 'default':
3792 self.openVideoApp.set_text('')
3793 self.openVideoApp.set_sensitive(True)
3794 self.openVideoApp.show()
3795 self.labelCustomVideoCommand.show()
3796 else:
3797 self.openVideoApp.set_text(command)
3798 self.openVideoApp.set_sensitive(False)
3799 self.openVideoApp.hide()
3800 self.labelCustomVideoCommand.hide()
3802 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3803 sens = not self.cbEnvironmentVariables.get_active()
3804 self.httpProxy.set_sensitive( sens)
3805 self.ftpProxy.set_sensitive( sens)
3807 def on_comboboxDeviceType_changed(self, widget, *args):
3808 active_item = self.comboboxDeviceType.get_active()
3810 # None
3811 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3812 self.imageSyncOptions, self. separatorSyncOptions,
3813 self.on_sync_mark_played, self.on_sync_delete,
3814 self.on_sync_leave, self.label_after_sync,
3815 self.delete_episodes_marked_played,
3816 self.player_supports_ogg )
3818 for widget in sync_widgets:
3819 if active_item == 0:
3820 widget.hide_all()
3821 else:
3822 widget.show_all()
3824 # iPod
3825 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3826 self.ipod_write_gtkpod_extended,
3827 self.ipod_delete_played_from_db)
3829 for widget in ipod_widgets:
3830 if active_item == 1:
3831 widget.show_all()
3832 else:
3833 widget.hide_all()
3835 # filesystem-based MP3 player
3836 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3837 self.cbChannelSubfolder, self.cbCustomSyncName,
3838 self.entryCustomSyncName, self.btnCustomSyncNameHelp,
3839 self.player_supports_ogg )
3841 for widget in fs_widgets:
3842 if active_item == 2:
3843 widget.show_all()
3844 else:
3845 widget.hide_all()
3847 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3848 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3849 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3850 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3851 fs.set_current_folder(self.iPodMountpoint.get_label())
3852 if fs.run() == gtk.RESPONSE_OK:
3853 self.iPodMountpoint.set_label( fs.get_filename())
3854 fs.destroy()
3856 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3857 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3858 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3859 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3860 fs.set_current_folder(self.filesystemMountpoint.get_label())
3861 if fs.run() == gtk.RESPONSE_OK:
3862 self.filesystemMountpoint.set_label( fs.get_filename())
3863 fs.destroy()
3865 def on_btnOK_clicked(self, widget, *args):
3866 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3867 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3869 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3870 new_download_dir = self.chooserDownloadTo.get_filename()
3871 download_dir_size = util.calculate_size( gl.downloaddir)
3872 download_dir_size_string = gl.format_filesize( download_dir_size)
3873 event = Event()
3875 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3876 dlg.vbox.set_spacing( 5)
3877 dlg.set_border_width( 5)
3879 label = gtk.Label()
3880 label.set_line_wrap( True)
3881 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3882 myprogressbar = gtk.ProgressBar()
3884 # put it all together
3885 dlg.vbox.pack_start( label)
3886 dlg.vbox.pack_end( myprogressbar)
3888 # switch windows
3889 dlg.show_all()
3890 self.gPodderProperties.hide_all()
3892 # hide action area and separator line
3893 dlg.action_area.hide()
3894 dlg.set_has_separator( False)
3896 args = ( new_download_dir, event, )
3898 thread = Thread( target = self.set_download_dir, args = args)
3899 thread.start()
3901 while not event.isSet():
3902 try:
3903 new_download_dir_size = util.calculate_size( new_download_dir)
3904 except:
3905 new_download_dir_size = 0
3906 if download_dir_size > 0:
3907 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3908 else:
3909 fract = 0.0
3910 if fract < 0.99:
3911 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3912 else:
3913 myprogressbar.set_text( _('Finishing... please wait.'))
3914 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3915 event.wait( 0.1)
3916 while gtk.events_pending():
3917 gtk.main_iteration( False)
3919 dlg.destroy()
3921 device_type = self.comboboxDeviceType.get_active()
3922 if device_type == 0:
3923 gl.config.device_type = 'none'
3924 elif device_type == 1:
3925 gl.config.device_type = 'ipod'
3926 elif device_type == 2:
3927 gl.config.device_type = 'filesystem'
3928 elif device_type == 3:
3929 gl.config.device_type = 'mtp'
3930 self.gPodderProperties.destroy()
3931 if self.callback_finished:
3932 self.callback_finished()
3935 class gPodderEpisode(BuilderWidget):
3936 finger_friendly_widgets = ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3938 def new(self):
3939 setattr(self, 'episode', None)
3940 setattr(self, 'download_callback', None)
3941 setattr(self, 'play_callback', None)
3942 self.gPodderEpisode.connect('delete-event', self.on_delete_event)
3943 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3944 self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
3945 if gl.config.enable_html_shownotes and \
3946 not gpodder.interface == gpodder.MAEMO:
3947 try:
3948 import gtkhtml2
3949 setattr(self, 'have_gtkhtml2', True)
3950 # Generate a HTML view and remove the textview
3951 setattr(self, 'htmlview', gtkhtml2.View())
3952 self.scrolled_window.remove(self.scrolled_window.get_child())
3953 self.scrolled_window.add(self.htmlview)
3954 self.textview = None
3955 self.htmlview.set_document(gtkhtml2.Document())
3956 self.htmlview.show()
3957 except ImportError:
3958 log('Install gtkhtml2 if you want HTML shownotes', sender=self)
3959 setattr(self, 'have_gtkhtml2', False)
3960 else:
3961 setattr(self, 'have_gtkhtml2', False)
3962 self.gPodderEpisode.connect('key-press-event', self.on_key_press)
3964 def on_key_press(self, widget, event):
3965 if not hasattr(self.scrolled_window, 'get_vscrollbar'):
3966 return
3967 vsb = self.scrolled_window.get_vscrollbar()
3968 vadj = vsb.get_adjustment()
3969 step = vadj.step_increment
3970 if event.keyval in (gtk.keysyms.J, gtk.keysyms.j):
3971 vsb.set_value(vsb.get_value() + step)
3972 elif event.keyval in (gtk.keysyms.K, gtk.keysyms.k):
3973 vsb.set_value(vsb.get_value() - step)
3975 def show(self, episode, download_callback, play_callback):
3976 self.download_progress.set_fraction(0)
3977 self.download_progress.set_text(_('Please wait...'))
3978 self.episode = episode
3979 self.download_callback = download_callback
3980 self.play_callback = play_callback
3982 self.gPodderEpisode.set_title(self.episode.title)
3984 if self.have_gtkhtml2:
3985 import gtkhtml2
3986 d = gtkhtml2.Document()
3987 d.open_stream('text/html')
3988 d.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3989 d.close_stream()
3990 self.htmlview.set_document(d)
3991 else:
3992 b = gtk.TextBuffer()
3993 self.textview.set_buffer(b)
3995 self.hide_show_widgets()
3996 self.gPodderEpisode.show()
3998 # Make sure the window comes up right now:
3999 while gtk.events_pending():
4000 gtk.main_iteration(False)
4002 # Now do the stuff that takes a bit longer...
4003 heading = self.episode.title
4004 subheading = 'from %s' % (self.episode.channel.title)
4005 description = self.episode.description
4006 footer = []
4008 if self.have_gtkhtml2:
4009 import gtkhtml2
4010 d.connect('link-clicked', lambda d, url: util.open_website(url))
4011 def request_url(document, url, stream):
4012 def opendata(url, stream):
4013 fp = urllib2.urlopen(url)
4014 data = fp.read(1024*10)
4015 while data != '':
4016 stream.write(data)
4017 data = fp.read(1024*10)
4018 stream.close()
4019 Thread(target=opendata, args=[url, stream]).start()
4020 d.connect('request-url', request_url)
4021 d.clear()
4022 d.open_stream('text/html')
4023 d.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
4024 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)))
4025 d.write_stream(self.episode.description)
4026 if len(footer):
4027 d.write_stream('<hr style="border: 1px #eeeeee solid;">')
4028 d.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils.escape(f) for f in footer))),))
4029 d.write_stream('</p></body></html>')
4030 d.close_stream()
4031 else:
4032 b.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
4033 b.create_tag('subheading', scale=pango.SCALE_SMALL)
4034 b.create_tag('footer', scale=pango.SCALE_SMALL)
4036 b.insert_with_tags_by_name(b.get_end_iter(), heading, 'heading')
4037 b.insert_at_cursor('\n')
4038 b.insert_with_tags_by_name(b.get_end_iter(), subheading, 'subheading')
4039 b.insert_at_cursor('\n\n')
4040 b.insert(b.get_end_iter(), util.remove_html_tags(description))
4041 if len(footer):
4042 b.insert_at_cursor('\n\n')
4043 b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
4044 b.place_cursor(b.get_start_iter())
4046 def on_cancel(self, widget):
4047 self.download_status_manager.cancel_by_url(self.episode.url)
4049 def on_delete_event(self, widget, event):
4050 # Avoid destroying the dialog, simply hide
4051 self.on_close(widget)
4052 return True
4054 def on_close(self, widget):
4055 self.episode = None
4056 if self.have_gtkhtml2:
4057 import gtkhtml2
4058 self.htmlview.set_document(gtkhtml2.Document())
4059 else:
4060 self.textview.get_buffer().set_text('')
4061 self.gPodderEpisode.hide()
4063 def download_status_changed(self, episode_urls):
4064 # Reload the episode from the database, so a newly-set local_filename
4065 # as a result of a download gets updated in the episode object
4066 self.episode.reload_from_db()
4067 self.hide_show_widgets()
4069 def download_status_progress(self, progress, speed):
4070 # We receive this from the main window every time the progress
4071 # for our episode has changed (but only when this window is visible)
4072 self.download_progress.set_fraction(progress)
4073 self.download_progress.set_text('Downloading: %d%% (%s/s)' % (100.*progress, gl.format_filesize(speed)))
4075 def hide_show_widgets(self):
4076 is_downloading = self.episode_is_downloading(self.episode)
4077 if is_downloading:
4078 self.download_progress.show_all()
4079 self.btnCancel.show_all()
4080 self.btnPlay.hide_all()
4081 self.btnDownload.hide_all()
4082 else:
4083 self.download_progress.hide_all()
4084 self.btnCancel.hide_all()
4085 if self.episode.was_downloaded(and_exists=True):
4086 if self.episode.file_type() in ('audio', 'video'):
4087 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
4088 else:
4089 self.btnPlay.set_label(gtk.STOCK_OPEN)
4090 self.btnPlay.set_use_stock(True)
4091 self.btnPlay.show_all()
4092 self.btnDownload.hide_all()
4093 else:
4094 self.btnPlay.hide_all()
4095 self.btnDownload.show_all()
4097 def on_download(self, widget):
4098 if self.download_callback:
4099 self.download_callback()
4101 def on_playback(self, widget):
4102 if self.play_callback:
4103 self.play_callback()
4104 self.on_close(widget)
4106 class gPodderSync(BuilderWidget):
4107 def new(self):
4108 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
4110 self.device.register('progress', self.on_progress)
4111 self.device.register('sub-progress', self.on_sub_progress)
4112 self.device.register('status', self.on_status)
4113 self.device.register('done', self.on_done)
4115 def on_progress(self, pos, max, text=None):
4116 if text is None:
4117 text = _('%d of %d done') % (pos, max)
4118 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
4119 util.idle_add(self.progressbar.set_text, text)
4121 def on_sub_progress(self, percentage):
4122 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
4124 def on_status(self, status):
4125 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
4127 def on_done(self):
4128 util.idle_add(self.gPodderSync.destroy)
4129 if not self.gPodder.minimized:
4130 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
4132 def on_gPodderSync_destroy(self, widget, *args):
4133 self.device.unregister('progress', self.on_progress)
4134 self.device.unregister('sub-progress', self.on_sub_progress)
4135 self.device.unregister('status', self.on_status)
4136 self.device.unregister('done', self.on_done)
4137 self.device.cancel()
4139 def on_cancel_button_clicked(self, widget, *args):
4140 self.device.cancel()
4143 class gPodderOpmlLister(BuilderWidget):
4144 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
4145 (MODE_DOWNLOAD, MODE_SEARCH) = range(2)
4147 def new(self):
4148 # initiate channels list
4149 self.channels = []
4150 self.callback_for_channel = None
4151 self.callback_finished = None
4153 if hasattr(self, 'custom_title'):
4154 self.gPodderOpmlLister.set_title(self.custom_title)
4155 if hasattr(self, 'hide_url_entry'):
4156 self.hboxOpmlUrlEntry.hide_all()
4157 new_parent = self.notebookChannelAdder.get_parent()
4158 new_parent.remove(self.notebookChannelAdder)
4159 self.vboxOpmlImport.reparent(new_parent)
4161 self.setup_treeview(self.treeviewChannelChooser)
4162 self.setup_treeview(self.treeviewTopPodcastsChooser)
4163 self.setup_treeview(self.treeviewYouTubeChooser)
4165 self.current_mode = self.MODE_DOWNLOAD
4167 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
4169 def setup_treeview(self, tv):
4170 togglecell = gtk.CellRendererToggle()
4171 togglecell.set_property( 'activatable', True)
4172 togglecell.connect( 'toggled', self.callback_edited)
4173 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
4175 titlecell = gtk.CellRendererText()
4176 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
4177 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
4179 for itemcolumn in ( togglecolumn, titlecolumn ):
4180 tv.append_column(itemcolumn)
4182 def callback_edited( self, cell, path):
4183 model = self.get_treeview().get_model()
4185 url = model[path][2]
4187 model[path][0] = not model[path][0]
4188 if model[path][0]:
4189 self.channels.append( url)
4190 else:
4191 self.channels.remove( url)
4193 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
4195 def on_entryURL_changed(self, editable):
4196 old_mode = self.current_mode
4197 self.current_mode = not editable.get_text().lower().startswith('http://')
4198 if self.current_mode == old_mode:
4199 return
4201 if self.current_mode == self.MODE_SEARCH:
4202 self.btnDownloadOpml.set_property('image', None)
4203 self.btnDownloadOpml.set_label(gtk.STOCK_FIND)
4204 self.btnDownloadOpml.set_use_stock(True)
4205 self.labelOpmlUrl.set_text(_('Search podcast.de:'))
4206 else:
4207 self.btnDownloadOpml.set_label(_('Download'))
4208 self.btnDownloadOpml.set_image(gtk.image_new_from_stock(gtk.STOCK_GOTO_BOTTOM, gtk.ICON_SIZE_BUTTON))
4209 self.btnDownloadOpml.set_use_stock(False)
4210 self.labelOpmlUrl.set_text(_('OPML:'))
4212 def get_selected_channels(self, tab=None):
4213 channels = []
4215 model = self.get_treeview(tab).get_model()
4216 if model is not None:
4217 for row in model:
4218 if row[0]:
4219 channels.append(row[2])
4221 return channels
4223 def on_change_tab(self, tab):
4224 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
4226 def thread_finished(self, model, tab=0):
4227 if tab == 1:
4228 tv = self.treeviewTopPodcastsChooser
4229 elif tab == 2:
4230 tv = self.treeviewYouTubeChooser
4231 self.entryYoutubeSearch.set_sensitive(True)
4232 self.btnSearchYouTube.set_sensitive(True)
4233 self.btnOK.set_sensitive(False)
4234 else:
4235 tv = self.treeviewChannelChooser
4236 self.btnDownloadOpml.set_sensitive(True)
4237 self.entryURL.set_sensitive(True)
4238 self.channels = []
4240 tv.set_model(model)
4241 tv.set_sensitive(True)
4243 def thread_func(self, tab=0):
4244 if tab == 1:
4245 model = opml.Importer(gl.config.toplist_url).get_model()
4246 if len(model) == 0:
4247 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4248 elif tab == 2:
4249 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
4250 if len(model) == 0:
4251 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
4252 else:
4253 url = self.entryURL.get_text()
4254 if not os.path.isfile(url) and not url.lower().startswith('http://'):
4255 log('Using podcast.de search')
4256 url = 'http://api.podcast.de/opml/podcasts/suche/%s' % (urllib.quote(url),)
4257 model = opml.Importer(url).get_model()
4258 if len(model) == 0:
4259 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
4261 util.idle_add(self.thread_finished, model, tab)
4263 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
4264 if callback_for_channel:
4265 self.callback_for_channel = callback_for_channel
4266 if callback_finished:
4267 self.callback_finished = callback_finished
4268 self.entryURL.set_text( url)
4269 self.btnDownloadOpml.set_sensitive( False)
4270 self.entryURL.set_sensitive( False)
4271 self.btnOK.set_sensitive( False)
4272 self.treeviewChannelChooser.set_sensitive( False)
4273 Thread( target = self.thread_func).start()
4274 Thread( target = lambda: self.thread_func(1)).start()
4276 def select_all( self, value ):
4277 enabled = False
4278 model = self.get_treeview().get_model()
4279 if model is not None:
4280 for row in model:
4281 row[0] = value
4282 if value:
4283 enabled = True
4284 self.btnOK.set_sensitive(enabled)
4286 def on_gPodderOpmlLister_destroy(self, widget, *args):
4287 pass
4289 def on_btnDownloadOpml_clicked(self, widget, *args):
4290 self.get_channels_from_url( self.entryURL.get_text())
4292 def on_btnSearchYouTube_clicked(self, widget, *args):
4293 self.entryYoutubeSearch.set_sensitive(False)
4294 self.treeviewYouTubeChooser.set_sensitive(False)
4295 self.btnSearchYouTube.set_sensitive(False)
4296 Thread(target = lambda: self.thread_func(2)).start()
4298 def on_btnSelectAll_clicked(self, widget, *args):
4299 self.select_all(True)
4301 def on_btnSelectNone_clicked(self, widget, *args):
4302 self.select_all(False)
4304 def on_btnOK_clicked(self, widget, *args):
4305 self.channels = self.get_selected_channels()
4306 self.gPodderOpmlLister.destroy()
4308 # add channels that have been selected
4309 for url in self.channels:
4310 if self.callback_for_channel:
4311 self.callback_for_channel( url)
4313 if self.callback_finished:
4314 util.idle_add(self.callback_finished)
4316 def on_btnCancel_clicked(self, widget, *args):
4317 self.gPodderOpmlLister.destroy()
4319 def on_entryYoutubeSearch_key_press_event(self, widget, event):
4320 if event.keyval == gtk.keysyms.Return:
4321 self.on_btnSearchYouTube_clicked(widget)
4323 def get_treeview(self, tab=None):
4324 if tab is None:
4325 tab = self.notebookChannelAdder.get_current_page()
4327 if tab == 0:
4328 return self.treeviewChannelChooser
4329 elif tab == 1:
4330 return self.treeviewTopPodcastsChooser
4331 else:
4332 return self.treeviewYouTubeChooser
4334 class gPodderEpisodeSelector( BuilderWidget):
4335 """Episode selection dialog
4337 Optional keyword arguments that modify the behaviour of this dialog:
4339 - callback: Function that takes 1 parameter which is a list of
4340 the selected episodes (or empty list when none selected)
4341 - remove_callback: Function that takes 1 parameter which is a list
4342 of episodes that should be "removed" (see below)
4343 (default is None, which means remove not possible)
4344 - remove_action: Label for the "remove" action (default is "Remove")
4345 - remove_finished: Callback after all remove callbacks have finished
4346 (default is None, also depends on remove_callback)
4347 It will get a list of episode URLs that have been
4348 removed, so the main UI can update those
4349 - episodes: List of episodes that are presented for selection
4350 - selected: (optional) List of boolean variables that define the
4351 default checked state for the given episodes
4352 - selected_default: (optional) The default boolean value for the
4353 checked state if no other value is set
4354 (default is False)
4355 - columns: List of (name, sort_name, sort_type, caption) pairs for the
4356 columns, the name is the attribute name of the episode to be
4357 read from each episode object. The sort name is the
4358 attribute name of the episode to be used to sort this column.
4359 If the sort_name is None it will use the attribute name for
4360 sorting. The sort type is the type of the sort column.
4361 The caption attribute is the text that appear as column caption
4362 (default is [('title_and_description', None, None, 'Episode'),])
4363 - title: (optional) The title of the window + heading
4364 - instructions: (optional) A one-line text describing what the
4365 user should select / what the selection is for
4366 - stock_ok_button: (optional) Will replace the "OK" button with
4367 another GTK+ stock item to be used for the
4368 affirmative button of the dialog (e.g. can
4369 be gtk.STOCK_DELETE when the episodes to be
4370 selected will be deleted after closing the
4371 dialog)
4372 - selection_buttons: (optional) A dictionary with labels as
4373 keys and callbacks as values; for each
4374 key a button will be generated, and when
4375 the button is clicked, the callback will
4376 be called for each episode and the return
4377 value of the callback (True or False) will
4378 be the new selected state of the episode
4379 - size_attribute: (optional) The name of an attribute of the
4380 supplied episode objects that can be used to
4381 calculate the size of an episode; set this to
4382 None if no total size calculation should be
4383 done (in cases where total size is useless)
4384 (default is 'length')
4385 - tooltip_attribute: (optional) The name of an attribute of
4386 the supplied episode objects that holds
4387 the text for the tooltips when hovering
4388 over an episode (default is 'description')
4391 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
4393 COLUMN_INDEX = 0
4394 COLUMN_TOOLTIP = 1
4395 COLUMN_TOGGLE = 2
4396 COLUMN_ADDITIONAL = 3
4398 def new( self):
4399 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
4400 if not hasattr( self, 'callback'):
4401 self.callback = None
4403 if not hasattr(self, 'remove_callback'):
4404 self.remove_callback = None
4406 if not hasattr(self, 'remove_action'):
4407 self.remove_action = _('Remove')
4409 if not hasattr(self, 'remove_finished'):
4410 self.remove_finished = None
4412 if not hasattr( self, 'episodes'):
4413 self.episodes = []
4415 if not hasattr( self, 'size_attribute'):
4416 self.size_attribute = 'length'
4418 if not hasattr(self, 'tooltip_attribute'):
4419 self.tooltip_attribute = 'description'
4421 if not hasattr( self, 'selection_buttons'):
4422 self.selection_buttons = {}
4424 if not hasattr( self, 'selected_default'):
4425 self.selected_default = False
4427 if not hasattr( self, 'selected'):
4428 self.selected = [self.selected_default]*len(self.episodes)
4430 if len(self.selected) < len(self.episodes):
4431 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
4433 if not hasattr( self, 'columns'):
4434 self.columns = (('title_and_description', None, None, _('Episode')),)
4436 if hasattr( self, 'title'):
4437 self.gPodderEpisodeSelector.set_title( self.title)
4438 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
4440 if gpodder.interface == gpodder.MAEMO:
4441 self.labelHeading.hide()
4443 if hasattr( self, 'instructions'):
4444 self.labelInstructions.set_text( self.instructions)
4445 self.labelInstructions.show_all()
4447 if hasattr(self, 'stock_ok_button'):
4448 if self.stock_ok_button == 'gpodder-download':
4449 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
4450 self.btnOK.set_label(_('Download'))
4451 else:
4452 self.btnOK.set_label(self.stock_ok_button)
4453 self.btnOK.set_use_stock(True)
4455 # check/uncheck column
4456 toggle_cell = gtk.CellRendererToggle()
4457 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
4458 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
4460 next_column = self.COLUMN_ADDITIONAL
4461 for name, sort_name, sort_type, caption in self.columns:
4462 renderer = gtk.CellRendererText()
4463 if next_column < self.COLUMN_ADDITIONAL + 2:
4464 renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
4465 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
4466 column.set_resizable( True)
4467 # Only set "expand" on the first two columns
4468 if next_column < self.COLUMN_ADDITIONAL + 2:
4469 column.set_expand(True)
4470 if sort_name is not None:
4471 column.set_sort_column_id(next_column+1)
4472 else:
4473 column.set_sort_column_id(next_column)
4474 self.treeviewEpisodes.append_column( column)
4475 next_column += 1
4477 if sort_name is not None:
4478 # add the sort column
4479 column = gtk.TreeViewColumn()
4480 column.set_visible(False)
4481 self.treeviewEpisodes.append_column( column)
4482 next_column += 1
4484 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
4485 # add string column type plus sort column type if it exists
4486 for name, sort_name, sort_type, caption in self.columns:
4487 column_types.append(gobject.TYPE_STRING)
4488 if sort_name is not None:
4489 column_types.append(sort_type)
4490 self.model = gtk.ListStore( *column_types)
4492 tooltip = None
4493 for index, episode in enumerate( self.episodes):
4494 if self.tooltip_attribute is not None:
4495 try:
4496 tooltip = getattr(episode, self.tooltip_attribute)
4497 except:
4498 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
4499 tooltip = None
4500 row = [ index, tooltip, self.selected[index] ]
4501 for name, sort_name, sort_type, caption in self.columns:
4502 if not hasattr(episode, name):
4503 log('Warning: Missing attribute "%s"', name, sender=self)
4504 row.append(None)
4505 else:
4506 row.append(getattr( episode, name))
4508 if sort_name is not None:
4509 if not hasattr(episode, sort_name):
4510 log('Warning: Missing attribute "%s"', sort_name, sender=self)
4511 row.append(None)
4512 else:
4513 row.append(getattr( episode, sort_name))
4514 self.model.append( row)
4516 if self.remove_callback is not None:
4517 self.btnRemoveAction.show()
4518 self.btnRemoveAction.set_label(self.remove_action)
4520 # connect to tooltip signals
4521 if self.tooltip_attribute is not None:
4522 try:
4523 self.treeviewEpisodes.set_property('has-tooltip', True)
4524 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
4525 except:
4526 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
4527 self.last_tooltip_episode = None
4528 self.episode_list_can_tooltip = True
4530 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
4531 self.treeviewEpisodes.set_rules_hint( True)
4532 self.treeviewEpisodes.set_model( self.model)
4533 self.treeviewEpisodes.columns_autosize()
4534 self.calculate_total_size()
4536 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
4537 # With get_bin_window, we get the window that contains the rows without
4538 # the header. The Y coordinate of this window will be the height of the
4539 # treeview header. This is the amount we have to subtract from the
4540 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
4541 (x_bin, y_bin) = treeview.get_bin_window().get_position()
4542 y -= x_bin
4543 y -= y_bin
4544 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
4546 if not self.episode_list_can_tooltip:
4547 self.last_tooltip_episode = None
4548 return False
4550 if path is not None:
4551 model = treeview.get_model()
4552 iter = model.get_iter(path)
4553 index = model.get_value(iter, self.COLUMN_INDEX)
4554 description = model.get_value(iter, self.COLUMN_TOOLTIP)
4555 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
4556 self.last_tooltip_episode = None
4557 return False
4558 self.last_tooltip_episode = index
4560 if description is not None:
4561 tooltip.set_text(description)
4562 return True
4563 else:
4564 return False
4566 self.last_tooltip_episode = None
4567 return False
4569 def treeview_episodes_button_pressed(self, treeview, event):
4570 if event.button == 3:
4571 menu = gtk.Menu()
4573 if len(self.selection_buttons):
4574 for label in self.selection_buttons:
4575 item = gtk.MenuItem(label)
4576 item.connect('activate', self.custom_selection_button_clicked, label)
4577 menu.append(item)
4578 menu.append(gtk.SeparatorMenuItem())
4580 item = gtk.MenuItem(_('Select all'))
4581 item.connect('activate', self.on_btnCheckAll_clicked)
4582 menu.append(item)
4584 item = gtk.MenuItem(_('Select none'))
4585 item.connect('activate', self.on_btnCheckNone_clicked)
4586 menu.append(item)
4588 menu.show_all()
4589 # Disable tooltips while we are showing the menu, so
4590 # the tooltip will not appear over the menu
4591 self.episode_list_can_tooltip = False
4592 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
4593 menu.popup(None, None, None, event.button, event.time)
4595 return True
4597 def episode_list_allow_tooltips(self):
4598 self.episode_list_can_tooltip = True
4600 def calculate_total_size( self):
4601 if self.size_attribute is not None:
4602 (total_size, count) = (0, 0)
4603 for episode in self.get_selected_episodes():
4604 try:
4605 total_size += int(getattr( episode, self.size_attribute))
4606 count += 1
4607 except:
4608 log( 'Cannot get size for %s', episode.title, sender = self)
4610 text = []
4611 if count == 0:
4612 text.append(_('Nothing selected'))
4613 elif count == 1:
4614 text.append(_('One episode selected'))
4615 else:
4616 text.append(_('%d episodes selected') % count)
4617 if total_size > 0:
4618 text.append(_('total size: %s') % gl.format_filesize(total_size))
4619 self.labelTotalSize.set_text(', '.join(text))
4620 self.btnOK.set_sensitive(count>0)
4621 self.btnRemoveAction.set_sensitive(count>0)
4622 if count > 0:
4623 self.btnCancel.set_label(gtk.STOCK_CANCEL)
4624 else:
4625 self.btnCancel.set_label(gtk.STOCK_CLOSE)
4626 else:
4627 self.btnOK.set_sensitive(False)
4628 self.btnRemoveAction.set_sensitive(False)
4629 for index, row in enumerate(self.model):
4630 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
4631 self.btnOK.set_sensitive(True)
4632 self.btnRemoveAction.set_sensitive(True)
4633 break
4634 self.labelTotalSize.set_text('')
4636 def toggle_cell_handler( self, cell, path):
4637 model = self.treeviewEpisodes.get_model()
4638 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
4640 self.calculate_total_size()
4642 def custom_selection_button_clicked(self, button, label):
4643 callback = self.selection_buttons[label]
4645 for index, row in enumerate( self.model):
4646 new_value = callback( self.episodes[index])
4647 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
4649 self.calculate_total_size()
4651 def on_btnCheckAll_clicked( self, widget):
4652 for row in self.model:
4653 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
4655 self.calculate_total_size()
4657 def on_btnCheckNone_clicked( self, widget):
4658 for row in self.model:
4659 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
4661 self.calculate_total_size()
4663 def on_remove_action_activate(self, widget):
4664 episodes = self.get_selected_episodes(remove_episodes=True)
4666 urls = []
4667 for episode in episodes:
4668 urls.append(episode.url)
4669 self.remove_callback(episode)
4671 if self.remove_finished is not None:
4672 self.remove_finished(urls)
4673 self.calculate_total_size()
4675 def get_selected_episodes( self, remove_episodes=False):
4676 selected_episodes = []
4678 for index, row in enumerate( self.model):
4679 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
4680 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
4682 if remove_episodes:
4683 for episode in selected_episodes:
4684 index = self.episodes.index(episode)
4685 iter = self.model.get_iter_first()
4686 while iter is not None:
4687 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
4688 self.model.remove(iter)
4689 break
4690 iter = self.model.iter_next(iter)
4692 return selected_episodes
4694 def on_btnOK_clicked( self, widget):
4695 self.gPodderEpisodeSelector.destroy()
4696 if self.callback is not None:
4697 self.callback( self.get_selected_episodes())
4699 def on_btnCancel_clicked( self, widget):
4700 self.gPodderEpisodeSelector.destroy()
4701 if self.callback is not None:
4702 self.callback([])
4704 class gPodderConfigEditor(BuilderWidget):
4705 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
4707 def new(self):
4708 name_column = gtk.TreeViewColumn(_('Setting'))
4709 name_renderer = gtk.CellRendererText()
4710 name_column.pack_start(name_renderer)
4711 name_column.add_attribute(name_renderer, 'text', 0)
4712 name_column.add_attribute(name_renderer, 'style', 5)
4713 self.configeditor.append_column(name_column)
4715 value_column = gtk.TreeViewColumn(_('Set to'))
4716 value_check_renderer = gtk.CellRendererToggle()
4717 value_column.pack_start(value_check_renderer, expand=False)
4718 value_column.add_attribute(value_check_renderer, 'active', 7)
4719 value_column.add_attribute(value_check_renderer, 'visible', 6)
4720 value_column.add_attribute(value_check_renderer, 'activatable', 6)
4721 value_check_renderer.connect('toggled', self.value_toggled)
4723 value_renderer = gtk.CellRendererText()
4724 value_column.pack_start(value_renderer)
4725 value_column.add_attribute(value_renderer, 'text', 2)
4726 value_column.add_attribute(value_renderer, 'visible', 4)
4727 value_column.add_attribute(value_renderer, 'editable', 4)
4728 value_column.add_attribute(value_renderer, 'style', 5)
4729 value_renderer.connect('edited', self.value_edited)
4730 self.configeditor.append_column(value_column)
4732 self.model = gl.config.model()
4733 self.filter = self.model.filter_new()
4734 self.filter.set_visible_func(self.visible_func)
4736 self.configeditor.set_model(self.filter)
4737 self.configeditor.set_rules_hint(True)
4738 self.configeditor.get_selection().connect( 'changed',
4739 self.on_configeditor_row_changed )
4741 def visible_func(self, model, iter, user_data=None):
4742 text = self.entryFilter.get_text().lower()
4743 if text == '':
4744 return True
4745 else:
4746 # either the variable name or its value
4747 return (text in model.get_value(iter, 0).lower() or
4748 text in model.get_value(iter, 2).lower())
4750 def value_edited(self, renderer, path, new_text):
4751 model = self.configeditor.get_model()
4752 iter = model.get_iter(path)
4753 name = model.get_value(iter, 0)
4754 type_cute = model.get_value(iter, 1)
4756 if not gl.config.update_field(name, new_text):
4757 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))
4759 def value_toggled(self, renderer, path):
4760 model = self.configeditor.get_model()
4761 iter = model.get_iter(path)
4762 field_name = model.get_value(iter, 0)
4763 field_type = model.get_value(iter, 3)
4765 # Flip the boolean config flag
4766 if field_type == bool:
4767 gl.config.toggle_flag(field_name)
4769 def on_entryFilter_changed(self, widget):
4770 self.filter.refilter()
4772 def on_btnShowAll_clicked(self, widget):
4773 self.entryFilter.set_text('')
4774 self.entryFilter.grab_focus()
4776 def on_btnClose_clicked(self, widget):
4777 self.gPodderConfigEditor.destroy()
4779 def on_configeditor_row_changed(self, treeselection):
4780 model, iter = treeselection.get_selected()
4781 if iter is not None:
4782 option_name = gl.config.get_description( model.get(iter, 0)[0] )
4783 self.config_option_description_label.set_text(option_name)
4785 class gPodderPlaylist(BuilderWidget):
4786 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4788 def new(self):
4789 self.linebreak = '\n'
4790 if gl.config.mp3_player_playlist_win_path:
4791 self.linebreak = '\r\n'
4792 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
4793 if self.mountpoint == '/':
4794 self.mountpoint = gl.config.mp3_player_folder
4795 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
4796 self.playlist_file = os.path.join(self.mountpoint,
4797 gl.config.mp3_player_playlist_file)
4798 icon_theme = gtk.icon_theme_get_default()
4799 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4801 # add column two
4802 check_cell = gtk.CellRendererToggle()
4803 check_cell.set_property('activatable', True)
4804 check_cell.connect('toggled', self.cell_toggled)
4805 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4806 self.treeviewPlaylist.append_column(check_column)
4808 # add column three
4809 column = gtk.TreeViewColumn(_('Filename'))
4810 icon_cell = gtk.CellRendererPixbuf()
4811 column.pack_start(icon_cell, False)
4812 column.add_attribute(icon_cell, 'pixbuf', 0)
4813 filename_cell = gtk.CellRendererText()
4814 column.pack_start(filename_cell, True)
4815 column.add_attribute(filename_cell, 'text', 2)
4817 column.set_resizable(True)
4818 self.treeviewPlaylist.append_column(column)
4820 # Make treeview reorderable
4821 self.treeviewPlaylist.set_reorderable(True)
4823 # init liststore
4824 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4825 self.treeviewPlaylist.set_model(self.playlist)
4827 # read device and playlist and fill the TreeView
4828 title = _('Reading files from %s') % gl.config.mp3_player_folder
4829 message = _('Please wait while gPodder reads your media file list from device.')
4830 dlg = gtk.MessageDialog(BuilderWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
4831 dlg.set_title(title)
4832 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
4833 dlg.show_all()
4834 Thread(target=self.process_device, args=[dlg]).start()
4836 def process_device(self, dlg):
4837 self.m3u = self.read_m3u()
4838 self.device = self.read_device()
4839 util.idle_add(self.write2gui, dlg)
4841 def cell_toggled(self, cellrenderertoggle, path):
4842 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4843 it = liststore.get_iter(path)
4844 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4846 def on_btnCancelPlaylist_clicked(self, widget):
4847 self.gPodderPlaylist.destroy()
4849 def on_btnSavePlaylist_clicked(self, widget):
4850 self.write_m3u()
4851 self.gPodderPlaylist.destroy()
4853 def read_m3u(self):
4855 read all files from the existing playlist
4857 tracks = []
4858 log("Read data from the playlistfile %s" % self.playlist_file)
4859 if os.path.exists(self.playlist_file):
4860 for line in open(self.playlist_file, 'r'):
4861 if not line.startswith('#EXT'):
4862 if line.startswith('#'):
4863 tracks.append([False, line[1:].strip()])
4864 else:
4865 tracks.append([True, line.strip()])
4866 return tracks
4868 def build_extinf(self, filename):
4869 if gl.config.mp3_player_playlist_win_path:
4870 filename = filename.replace('\\', os.sep)
4872 # rebuild the whole filename including the mountpoint
4873 if gl.config.mp3_player_playlist_absolute_path:
4874 absfile = self.mountpoint + filename
4875 else:
4876 absfile = util.rel2abs(filename, os.path.dirname(self.playlist_file))
4878 # read the title from the mp3/ogg tag
4879 metadata = libtagupdate.get_tags_from_file(absfile)
4880 if 'title' in metadata and metadata['title']:
4881 title = metadata['title']
4882 else:
4883 # fallback: use the basename of the file
4884 (title, extension) = os.path.splitext(os.path.basename(filename))
4886 return "#EXTINF:0,%s%s" % (title.strip(), self.linebreak)
4888 def write_m3u(self):
4890 write the list into the playlist on the device
4892 log('Writing playlist file: %s', self.playlist_file, sender=self)
4893 playlist_folder = os.path.split(self.playlist_file)[0]
4894 if not util.make_directory(playlist_folder):
4895 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4896 else:
4897 try:
4898 fp = open(self.playlist_file, 'w')
4899 fp.write('#EXTM3U%s' % self.linebreak)
4900 for icon, checked, filename in self.playlist:
4901 fp.write(self.build_extinf(filename))
4902 if not checked:
4903 fp.write('#')
4904 fp.write(filename)
4905 fp.write(self.linebreak)
4906 fp.close()
4907 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4908 except IOError, ioe:
4909 self.show_message(str(ioe), _('Error writing playlist file'))
4911 def read_device(self):
4913 read all files from the device
4915 log('Reading files from %s', gl.config.mp3_player_folder, sender=self)
4916 tracks = []
4917 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4918 for file in files:
4919 filename = os.path.join(root, file)
4921 if filename == self.playlist_file:
4922 # We don't want to have our playlist file as
4923 # an entry in our file list, so skip it!
4924 break
4926 if gl.config.mp3_player_playlist_absolute_path:
4927 filename = filename[len(self.mountpoint):]
4928 else:
4929 filename = util.relpath(os.path.dirname(self.playlist_file),
4930 os.path.dirname(filename)) + \
4931 os.sep + os.path.basename(filename)
4933 if gl.config.mp3_player_playlist_win_path:
4934 filename = filename.replace(os.sep, '\\')
4936 tracks.append(filename)
4937 return tracks
4939 def write2gui(self, dlg):
4940 # add the files from the device to the list only when
4941 # they are not yet in the playlist
4942 # mark this files as NEW
4943 for filename in self.device[:]:
4944 m3ulist = [file[1] for file in self.m3u]
4945 if filename not in m3ulist:
4946 self.playlist.append([self.icon_new, False, filename])
4948 # add the files from the playlist to the list only when
4949 # they are on the device
4950 for checked, filename in self.m3u[:]:
4951 if filename in self.device:
4952 self.playlist.append([None, checked, filename])
4954 dlg.destroy()
4955 return False
4957 class gPodderDependencyManager(BuilderWidget):
4958 def new(self):
4959 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4960 self.treeview_components.append_column(col_name)
4961 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4962 self.treeview_components.append_column(col_installed)
4963 self.treeview_components.set_model(services.dependency_manager.get_model())
4964 self.btn_about.set_sensitive(False)
4966 def on_btn_about_clicked(self, widget):
4967 selection = self.treeview_components.get_selection()
4968 model, iter = selection.get_selected()
4969 if iter is not None:
4970 title = model.get_value(iter, 0)
4971 description = model.get_value(iter, 1)
4972 available = model.get_value(iter, 3)
4973 missing = model.get_value(iter, 4)
4975 if not available:
4976 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4978 self.show_message(description, title)
4980 def on_btn_install_clicked(self, widget):
4981 # TODO: Implement package manager integration
4982 pass
4984 def on_treeview_components_cursor_changed(self, treeview):
4985 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4986 # TODO: If installing is possible, enable btn_install
4988 def on_gPodderDependencyManager_response(self, dialog, response_id):
4989 self.gPodderDependencyManager.destroy()
4991 class gPodderWelcome(BuilderWidget):
4992 finger_friendly_widgets = ['btnOPML', 'btnMygPodder', 'btnCancel']
4994 def new(self):
4995 self.gPodderWelcome.show()
4997 def on_show_example_podcasts(self, button):
4998 self.gPodderWelcome.destroy()
4999 self.show_example_podcasts_callback(None)
5001 def on_setup_my_gpodder(self, gpodder):
5002 self.gPodderWelcome.destroy()
5003 self.setup_my_gpodder_callback(None)
5005 def on_btnCancel_clicked(self, button):
5006 self.gPodderWelcome.destroy()
5008 def main():
5009 gobject.threads_init()
5010 gtk.window_set_default_icon_name( 'gpodder')
5012 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
5013 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
5015 if gpodder.interface == gpodder.MAEMO and \
5016 not gl.config.disable_fingerscroll:
5017 uibase.GtkBuilderWidget.use_fingerscroll = True
5019 gp = gPodder(bus_name)
5020 gp.run()