M3U write support for Sandisk Sansa (bug 251)
[gpodder.git] / src / gpodder / gui.py
blob63c9eafdb92319b23da5b69f2f38ec77e6a14225
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import libtagupdate
43 from gpodder import util
44 from gpodder import opml
45 from gpodder import services
46 from gpodder import sync
47 from gpodder import download
48 from gpodder import SimpleGladeApp
49 from gpodder import my
50 from gpodder.liblogger import log
51 from gpodder.dbsqlite import db
52 from gpodder import resolver
54 try:
55 from gpodder import trayicon
56 have_trayicon = True
57 except Exception, exc:
58 log('Warning: Could not import gpodder.trayicon.', traceback=True)
59 log('Warning: This probably means your PyGTK installation is too old!')
60 have_trayicon = False
62 from libpodcasts import podcastChannel
63 from libpodcasts import LocalDBReader
64 from libpodcasts import podcastItem
65 from libpodcasts import channels_to_model
66 from libpodcasts import update_channel_model_by_iter
67 from libpodcasts import load_channels
68 from libpodcasts import update_channels
69 from libpodcasts import save_channels
70 from libpodcasts import can_restore_from_opml
71 from libpodcasts import HTTPAuthError
73 from gpodder.libgpodder import gl
75 from libplayers import UserAppsReader
77 from libtagupdate import tagging_supported
79 if gpodder.interface == gpodder.GUI:
80 WEB_BROWSER_ICON = 'web-browser'
81 elif gpodder.interface == gpodder.MAEMO:
82 import hildon
83 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
85 app_name = "gpodder"
86 app_version = "unknown" # will be set in main() call
87 app_authors = [
88 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
89 '',
90 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
91 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
92 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
93 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
94 'Carlos Moffat', 'Chris Arnold', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
95 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'FFranci72', 'Florian Richter', 'Frank Harper',
96 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
97 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
98 'Jérôme Chabod', 'Jerry Moss',
99 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
100 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
101 'Jürgen Schinker', 'Justin Forest',
102 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
103 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Mykola Nikishov', 'narf',
104 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
105 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
106 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
107 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
108 'Robert Young', 'Roel Groeneveld',
109 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
110 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
111 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
112 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
113 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
115 'List may be incomplete - please contact me.'
117 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
118 app_website = 'http://www.gpodder.org/'
120 # these will be filled with pathnames in bin/gpodder
121 glade_dir = [ 'share', 'gpodder' ]
122 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
123 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
126 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
127 gpodder_main_window = None
128 finger_friendly_widgets = []
130 def __init__( self, **kwargs):
131 path = os.path.join( glade_dir, '%s.glade' % app_name)
132 root = self.__class__.__name__
133 domain = app_name
135 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
137 # Set widgets to finger-friendly mode if on Maemo
138 for widget_name in self.finger_friendly_widgets:
139 if hasattr(self, widget_name):
140 self.set_finger_friendly(getattr(self, widget_name))
141 else:
142 log('Finger-friendly widget not found: %s', widget_name, sender=self)
144 if root == 'gPodder':
145 GladeWidget.gpodder_main_window = self.gPodder
146 else:
147 # If we have a child window, set it transient for our main window
148 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
150 if gpodder.interface == gpodder.GUI:
151 if hasattr( self, 'center_on_widget'):
152 ( x, y ) = self.gpodder_main_window.get_position()
153 a = self.center_on_widget.allocation
154 ( x, y ) = ( x + a.x, y + a.y )
155 ( w, h ) = ( a.width, a.height )
156 ( pw, ph ) = getattr( self, root).get_size()
157 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
158 else:
159 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
161 def notification(self, message, title=None):
162 util.idle_add(self.show_message, message, title)
164 def show_message( self, message, title = None):
165 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
166 if title is None:
167 title = 'gPodder'
168 self.tray_icon.send_notification(message, title)
169 return
171 if gpodder.interface == gpodder.GUI:
172 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
173 if title:
174 dlg.set_title(str(title))
175 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
176 else:
177 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
178 elif gpodder.interface == gpodder.MAEMO:
179 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
181 dlg.run()
182 dlg.destroy()
184 def set_finger_friendly(self, widget):
186 If we are on Maemo, we carry out the necessary
187 operations to turn a widget into a finger-friendly
188 one, depending on which type of widget it is (i.e.
189 buttons will have more padding, TreeViews a thick
190 scrollbar, etc..)
192 if gpodder.interface == gpodder.MAEMO:
193 if isinstance(widget, gtk.Misc):
194 widget.set_padding(0, 5)
195 elif isinstance(widget, gtk.Button):
196 for child in widget.get_children():
197 if isinstance(child, gtk.Alignment):
198 child.set_padding(5, 5, 5, 5)
199 else:
200 child.set_padding(5, 5)
201 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
202 parent = widget.get_parent()
203 if isinstance(parent, gtk.ScrolledWindow):
204 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
205 elif isinstance(widget, gtk.MenuItem):
206 for child in widget.get_children():
207 self.set_finger_friendly(child)
208 submenu = widget.get_submenu()
209 if submenu is not None:
210 for child in submenu.get_children():
211 self.set_finger_friendly(child)
212 elif isinstance(widget, gtk.Menu):
213 for child in widget.get_children():
214 self.set_finger_friendly(child)
215 else:
216 log('Cannot set widget finger-friendly: %s', widget, sender=self)
218 return widget
220 def show_confirmation( self, message, title = None):
221 if gpodder.interface == gpodder.GUI:
222 affirmative = gtk.RESPONSE_YES
223 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
224 if title:
225 dlg.set_title(str(title))
226 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
227 else:
228 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
229 elif gpodder.interface == gpodder.MAEMO:
230 affirmative = gtk.RESPONSE_OK
231 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
233 response = dlg.run()
234 dlg.destroy()
236 return response == affirmative
238 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
239 """ An authentication dialog based on
240 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
242 dialog = gtk.MessageDialog(
243 GladeWidget.gpodder_main_window,
244 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
245 gtk.MESSAGE_QUESTION,
246 gtk.BUTTONS_OK_CANCEL )
248 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
250 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
251 dialog.set_title(_('Authentication required'))
252 dialog.format_secondary_markup(message)
253 dialog.set_default_response(gtk.RESPONSE_OK)
255 if register_callback is not None:
256 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
258 username_entry = gtk.Entry()
259 password_entry = gtk.Entry()
261 username_entry.connect('activate', lambda w: password_entry.grab_focus())
262 password_entry.set_visibility(False)
263 password_entry.set_activates_default(True)
265 if username is not None:
266 username_entry.set_text(username)
267 if password is not None:
268 password_entry.set_text(password)
270 table = gtk.Table(2, 2)
271 table.set_row_spacings(6)
272 table.set_col_spacings(6)
274 username_label = gtk.Label()
275 username_label.set_markup('<b>' + username_prompt + ':</b>')
276 username_label.set_alignment(0.0, 0.5)
277 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
278 table.attach(username_entry, 1, 2, 0, 1)
280 password_label = gtk.Label()
281 password_label.set_markup('<b>' + _('Password') + ':</b>')
282 password_label.set_alignment(0.0, 0.5)
283 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
284 table.attach(password_entry, 1, 2, 1, 2)
286 dialog.vbox.pack_end(table, True, True, 0)
287 dialog.show_all()
288 response = dialog.run()
290 while response == gtk.RESPONSE_HELP:
291 register_callback()
292 response = dialog.run()
294 password_entry.set_visibility(True)
295 dialog.destroy()
297 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
299 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
300 if dst_filename is None:
301 dst_filename = src_filename
303 if dst_directory is None:
304 dst_directory = os.path.expanduser( '~')
306 ( base, extension ) = os.path.splitext( src_filename)
308 if not dst_filename.endswith( extension):
309 dst_filename += extension
311 if gpodder.interface == gpodder.GUI:
312 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
313 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
314 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
315 elif gpodder.interface == gpodder.MAEMO:
316 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
318 dlg.set_do_overwrite_confirmation( True)
319 dlg.set_current_name( os.path.basename( dst_filename))
320 dlg.set_current_folder( dst_directory)
322 result = False
323 folder = dst_directory
324 if dlg.run() == gtk.RESPONSE_OK:
325 result = True
326 dst_filename = dlg.get_filename()
327 folder = dlg.get_current_folder()
328 if not dst_filename.endswith( extension):
329 dst_filename += extension
331 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
333 try:
334 shutil.copyfile( src_filename, dst_filename)
335 except:
336 log( 'Error copying file.', sender = self, traceback = True)
338 dlg.destroy()
339 return (result, folder)
342 class gPodder(GladeWidget):
343 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'itemQuit', 'menuPodcasts', 'advanced1', 'menuChannels', 'menuHelp']
344 ENTER_URL_TEXT = _('Enter podcast URL...')
346 def new(self):
347 if gpodder.interface == gpodder.MAEMO:
348 # Maemo-specific changes to the UI
349 global scalable_dir
350 scalable_dir = scalable_dir.replace('.svg', '.png')
352 self.app = hildon.Program()
353 gtk.set_application_name('gPodder')
354 self.window = hildon.Window()
355 self.window.connect('delete-event', self.on_gPodder_delete_event)
356 self.window.connect('window-state-event', self.window_state_event)
358 self.itemUpdateChannel.show()
359 self.UpdateChannelSeparator.show()
361 # Give toolbar to the hildon window
362 self.toolbar.parent.remove(self.toolbar)
363 self.window.add_toolbar(self.toolbar)
365 # START TEMPORARY FIX FOR TOOLBAR STYLE
366 # It seems like libglade for python still mixes
367 # old GtkToolbar API with new ones - maybe this
368 # is the reason why setting the style doesn't
369 # work very well. This small hack fixes that :)
370 self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
371 def remove_label(w):
372 if hasattr(w, 'set_label'):
373 w.set_label(None)
374 self.toolbar.foreach(remove_label)
375 # END TEMPORARY FIX FOR TOOLBAR STYLE
377 self.app.add_window(self.window)
378 self.vMain.reparent(self.window)
379 self.gPodder = self.window
381 # Reparent the main menu
382 menu = gtk.Menu()
383 for child in self.mainMenu.get_children():
384 child.reparent(menu)
385 self.itemQuit.reparent(menu)
386 self.window.set_menu(menu)
388 self.mainMenu.destroy()
389 self.window.show()
391 # do some widget hiding
392 self.toolbar.remove(self.toolTransfer)
393 self.itemTransferSelected.hide_all()
394 self.item_email_subscriptions.hide_all()
396 # Feed cache update button
397 self.label120.set_text(_('Update'))
399 # get screen real estate
400 self.hboxContainer.set_border_width(0)
402 # Offer importing of videocenter podcasts
403 if os.path.exists(os.path.expanduser('~/videocenter')):
404 self.item_upgrade_from_videocenter.show()
405 self.upgrade_from_videocenter_separator.show()
407 self.gPodder.connect('key-press-event', self.on_key_press)
408 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
410 if gl.config.show_url_entry_in_podcast_list:
411 self.hboxAddChannel.show()
413 if not gl.config.show_toolbar:
414 self.toolbar.hide()
416 gl.config.add_observer(self.on_config_changed)
417 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
418 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
419 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
420 self.entry_add_channel_unfocus(self.entryAddChannel, None)
422 self.uar = None
423 self.tray_icon = None
424 self.gpodder_episode_window = None
426 self.fullscreen = False
427 self.minimized = False
428 self.gPodder.connect('window-state-event', self.window_state_event)
430 self.already_notified_new_episodes = []
431 self.show_hide_tray_icon()
432 self.no_episode_selected.set_sensitive(False)
434 self.itemShowToolbar.set_active(gl.config.show_toolbar)
435 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
437 gl.config.connect_gtk_window(self.gPodder, 'main_window')
438 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
440 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
441 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
442 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
443 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
445 # Make sure we free/close the download queue when we
446 # update the "max downloads" spin button
447 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
448 self.spinMaxDownloads.connect('value-changed', changed_cb)
450 self.default_title = None
451 if app_version.rfind('git') != -1:
452 self.set_title('gPodder %s' % app_version)
453 else:
454 title = self.gPodder.get_title()
455 if title is not None:
456 self.set_title(title)
457 else:
458 self.set_title(_('gPodder'))
460 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
462 # cell renderers for channel tree
463 iconcolumn = gtk.TreeViewColumn('')
465 iconcell = gtk.CellRendererPixbuf()
466 iconcolumn.pack_start( iconcell, False)
467 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
468 self.cell_channel_icon = iconcell
470 namecolumn = gtk.TreeViewColumn('')
471 namecell = gtk.CellRendererText()
472 namecell.set_property('foreground-set', True)
473 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
474 namecolumn.pack_start( namecell, True)
475 namecolumn.add_attribute( namecell, 'markup', 2)
476 namecolumn.add_attribute( namecell, 'foreground', 8)
478 iconcell = gtk.CellRendererPixbuf()
479 iconcell.set_property('xalign', 1.0)
480 namecolumn.pack_start( iconcell, False)
481 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
482 namecolumn.add_attribute(iconcell, 'visible', 7)
483 self.cell_channel_pill = iconcell
485 self.treeChannels.append_column(iconcolumn)
486 self.treeChannels.append_column(namecolumn)
487 self.treeChannels.set_headers_visible(False)
489 # enable alternating colors hint
490 self.treeAvailable.set_rules_hint( True)
491 self.treeChannels.set_rules_hint( True)
493 # connect to tooltip signals
494 try:
495 self.treeChannels.set_property('has-tooltip', True)
496 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
497 self.treeAvailable.set_property('has-tooltip', True)
498 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
499 except:
500 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
501 self.last_tooltip_channel = None
502 self.last_tooltip_episode = None
503 self.podcast_list_can_tooltip = True
504 self.episode_list_can_tooltip = True
506 # Add our context menu to treeAvailable
507 if gpodder.interface == gpodder.MAEMO:
508 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
509 else:
510 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
511 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
513 iconcell = gtk.CellRendererPixbuf()
514 if gpodder.interface == gpodder.MAEMO:
515 status_column_label = ''
516 else:
517 status_column_label = _('Status')
518 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
520 namecell = gtk.CellRendererText()
521 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
522 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
523 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
524 namecolumn.set_expand(True)
526 sizecell = gtk.CellRendererText()
527 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
529 releasecell = gtk.CellRendererText()
530 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
532 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
533 itemcolumn.set_reorderable(True)
534 self.treeAvailable.append_column(itemcolumn)
536 if gpodder.interface == gpodder.MAEMO:
537 # Due to screen space contraints, we
538 # hide these columns here by default
539 self.column_size = sizecolumn
540 self.column_released = releasecolumn
541 self.column_released.set_visible(False)
542 self.column_size.set_visible(False)
544 # enable search in treeavailable
545 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
547 # enable multiple selection support
548 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
549 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
551 # columns and renderers for "download progress" tab
552 episodecell = gtk.CellRendererText()
553 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
554 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
555 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
556 episodecolumn.set_expand(True)
558 speedcell = gtk.CellRendererText()
559 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
561 progresscell = gtk.CellRendererProgress()
562 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
563 progresscolumn.set_expand(True)
565 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
566 self.treeDownloads.append_column( itemcolumn)
568 # After we've set up most of the window, show it :)
569 if not gpodder.interface == gpodder.MAEMO:
570 self.gPodder.show()
572 if self.tray_icon:
573 if gl.config.start_iconified:
574 self.iconify_main_window()
575 elif gl.config.minimize_to_tray:
576 self.tray_icon.set_visible(False)
578 # a dictionary that maps episode URLs to the current
579 # treeAvailable row numbers to generate tree paths
580 self.url_path_mapping = {}
582 # a dictionary that maps channel URLs to the current
583 # treeChannels row numbers to generate tree paths
584 self.channel_url_path_mapping = {}
586 services.download_status_manager.register( 'list-changed', self.download_status_updated)
587 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
588 services.cover_downloader.register('cover-available', self.cover_download_finished)
589 services.cover_downloader.register('cover-removed', self.cover_file_removed)
590 self.cover_cache = {}
592 self.treeDownloads.set_model( services.download_status_manager.tree_model)
594 #Add Drag and Drop Support
595 flags = gtk.DEST_DEFAULT_ALL
596 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
597 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
598 self.treeChannels.drag_dest_set( flags, targets, actions)
599 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
601 # Subscribed channels
602 self.active_channel = None
603 self.channels = load_channels()
604 self.channel_list_changed = True
605 self.update_podcasts_tab()
607 # load list of user applications for audio playback
608 self.user_apps_reader = UserAppsReader(['audio', 'video'])
609 Thread(target=self.read_apps).start()
611 # Clean up old, orphaned download files
612 gl.clean_up_downloads( delete_partial = True)
614 # Set the "Device" menu item for the first time
615 self.update_item_device()
617 # Last folder used for saving episodes
618 self.folder_for_saving_episodes = None
620 # Set up default channel colors
621 self.channel_colors = {
622 'default': None,
623 'updating': gl.config.color_updating_feeds,
624 'parse_error': '#ff0000',
627 # Now, update the feed cache, when everything's in place
628 self.btnUpdateFeeds.show_all()
629 self.updated_feeds = 0
630 self.updating_feed_cache = False
631 self.feed_cache_update_cancelled = False
632 self.update_feed_cache(force_update=gl.config.update_on_startup)
634 # Start the auto-update procedure
635 self.auto_update_procedure(first_run=True)
637 # Delete old episodes if the user wishes to
638 if gl.config.auto_remove_old_episodes:
639 old_episodes = self.get_old_episodes()
640 if len(old_episodes) > 0:
641 self.delete_episode_list(old_episodes, confirm=False)
642 self.updateComboBox()
644 # First-time users should be asked if they want to see the OPML
645 if len(self.channels) == 0:
646 util.idle_add(self.on_itemUpdate_activate, None)
648 def on_tree_channels_resize(self, widget, allocation):
649 if not gl.config.podcast_sidebar_save_space:
650 return
652 window_allocation = self.gPodder.get_allocation()
653 percentage = 100. * float(allocation.width) / float(window_allocation.width)
654 if hasattr(self, 'cell_channel_icon'):
655 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
656 if hasattr(self, 'cell_channel_pill'):
657 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
659 def entry_add_channel_focus(self, widget, event):
660 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
661 if widget.get_text() == self.ENTER_URL_TEXT:
662 widget.set_text('')
664 def entry_add_channel_unfocus(self, widget, event):
665 if widget.get_text() == '':
666 widget.set_text(self.ENTER_URL_TEXT)
667 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
669 def on_config_changed(self, name, old_value, new_value):
670 if name == 'show_toolbar':
671 if new_value:
672 self.toolbar.show()
673 else:
674 self.toolbar.hide()
675 elif name == 'episode_list_descriptions':
676 self.updateTreeView()
677 elif name == 'show_url_entry_in_podcast_list':
678 if new_value:
679 self.hboxAddChannel.show()
680 else:
681 self.hboxAddChannel.hide()
683 def read_apps(self):
684 time.sleep(3) # give other parts of gpodder a chance to start up
685 self.user_apps_reader.read()
686 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
687 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
689 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
690 # With get_bin_window, we get the window that contains the rows without
691 # the header. The Y coordinate of this window will be the height of the
692 # treeview header. This is the amount we have to subtract from the
693 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
694 (x_bin, y_bin) = treeview.get_bin_window().get_position()
695 y -= x_bin
696 y -= y_bin
697 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
699 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
700 self.last_tooltip_episode = None
701 return False
703 if path is not None:
704 model = treeview.get_model()
705 iter = model.get_iter(path)
706 url = model.get_value(iter, 0)
707 description = model.get_value(iter, 7)
708 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
709 self.last_tooltip_episode = None
710 return False
711 self.last_tooltip_episode = url
713 if len(description) > 400:
714 description = description[:398]+'[...]'
716 tooltip.set_text(description)
717 return True
719 self.last_tooltip_episode = None
720 return False
722 def podcast_list_allow_tooltips(self):
723 self.podcast_list_can_tooltip = True
725 def episode_list_allow_tooltips(self):
726 self.episode_list_can_tooltip = True
728 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
729 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
731 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
732 self.last_tooltip_channel = None
733 return False
735 if path is not None:
736 model = treeview.get_model()
737 iter = model.get_iter(path)
738 url = model.get_value(iter, 0)
739 for channel in self.channels:
740 if channel.url == url:
741 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
742 self.last_tooltip_channel = None
743 return False
744 self.last_tooltip_channel = channel
745 channel.request_save_dir_size()
746 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
747 error_str = model.get_value(iter, 6)
748 if error_str:
749 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
750 error_str = '<span foreground="#ff0000">%s</span>' % error_str
751 table = gtk.Table(rows=3, columns=3)
752 table.set_row_spacings(5)
753 table.set_col_spacings(5)
754 table.set_border_width(5)
756 heading = gtk.Label()
757 heading.set_alignment(0, 1)
758 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
759 table.attach(heading, 0, 1, 0, 1)
760 size_info = gtk.Label()
761 size_info.set_alignment(1, 1)
762 size_info.set_justify(gtk.JUSTIFY_RIGHT)
763 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
764 table.attach(size_info, 2, 3, 0, 1)
766 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
768 if len(channel.description) < 500:
769 description = channel.description
770 else:
771 pos = channel.description.find('\n\n')
772 if pos == -1 or pos > 500:
773 description = channel.description[:498]+'[...]'
774 else:
775 description = channel.description[:pos]
777 description = gtk.Label(description)
778 if error_str:
779 description.set_markup(error_str)
780 description.set_alignment(0, 0)
781 description.set_line_wrap(True)
782 table.attach(description, 0, 3, 2, 3)
784 table.show_all()
785 tooltip.set_custom(table)
787 return True
789 self.last_tooltip_channel = None
790 return False
792 def update_m3u_playlist_clicked(self, widget):
793 self.active_channel.update_m3u_playlist()
794 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
796 def treeview_channels_button_pressed( self, treeview, event):
797 global WEB_BROWSER_ICON
799 if event.button == 3:
800 ( x, y ) = ( int(event.x), int(event.y) )
801 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
803 paths = []
805 # Did the user right-click into a selection?
806 selection = treeview.get_selection()
807 if selection.count_selected_rows() and path:
808 ( model, paths ) = selection.get_selected_rows()
809 if path not in paths:
810 # We have right-clicked, but not into the
811 # selection, assume we don't want to operate
812 # on the selection
813 paths = []
815 # No selection or right click not in selection:
816 # Select the single item where we clicked
817 if not len( paths) and path:
818 treeview.grab_focus()
819 treeview.set_cursor( path, column, 0)
821 ( model, paths ) = ( treeview.get_model(), [ path ] )
823 # We did not find a selection, and the user didn't
824 # click on an item to select -- don't show the menu
825 if not len( paths):
826 return True
828 menu = gtk.Menu()
830 item = gtk.ImageMenuItem( _('Open download folder'))
831 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
832 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
833 menu.append( item)
835 item = gtk.ImageMenuItem( _('Update Feed'))
836 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
837 item.connect('activate', self.on_itemUpdateChannel_activate )
838 item.set_sensitive( not self.updating_feed_cache )
839 menu.append( item)
841 if gl.config.create_m3u_playlists:
842 item = gtk.ImageMenuItem(_('Update M3U playlist'))
843 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
844 item.connect('activate', self.update_m3u_playlist_clicked)
845 menu.append(item)
847 if self.active_channel.link:
848 item = gtk.ImageMenuItem(_('Visit website'))
849 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
850 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
851 menu.append(item)
853 if self.active_channel.channel_is_locked:
854 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
855 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
856 item.connect('activate', self.on_channel_toggle_lock_activate)
857 menu.append(self.set_finger_friendly(item))
858 else:
859 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
860 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
861 item.connect('activate', self.on_channel_toggle_lock_activate)
862 menu.append(self.set_finger_friendly(item))
865 menu.append( gtk.SeparatorMenuItem())
867 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
868 item.connect( 'activate', self.on_itemEditChannel_activate)
869 menu.append( item)
871 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
872 item.connect( 'activate', self.on_itemRemoveChannel_activate)
873 menu.append( item)
875 menu.show_all()
876 # Disable tooltips while we are showing the menu, so
877 # the tooltip will not appear over the menu
878 self.podcast_list_can_tooltip = False
879 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
880 menu.popup( None, None, None, event.button, event.time)
882 return True
884 def on_itemClose_activate(self, widget):
885 if self.tray_icon is not None:
886 if gpodder.interface == gpodder.MAEMO:
887 self.gPodder.set_property('visible', False)
888 else:
889 self.iconify_main_window()
890 else:
891 self.on_gPodder_delete_event(widget)
893 def cover_file_removed(self, channel_url):
895 The Cover Downloader calls this when a previously-
896 available cover has been removed from the disk. We
897 have to update our cache to reflect this change.
899 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
900 for row in self.treeChannels.get_model():
901 if row[COLUMN_URL] == channel_url:
902 row[COLUMN_PIXBUF] = None
903 key = (channel_url, gl.config.podcast_list_icon_size, \
904 gl.config.podcast_list_icon_size)
905 if key in self.cover_cache:
906 del self.cover_cache[key]
909 def cover_download_finished(self, channel_url, pixbuf):
911 The Cover Downloader calls this when it has finished
912 downloading (or registering, if already downloaded)
913 a new channel cover, which is ready for displaying.
915 if pixbuf is not None:
916 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
917 for row in self.treeChannels.get_model():
918 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
919 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)
920 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
922 def save_episode_as_file( self, url, *args):
923 episode = self.active_channel.find_episode(url)
925 folder = self.folder_for_saving_episodes
926 (result, folder) = self.show_copy_dialog(src_filename=episode.local_filename(), dst_filename=episode.sync_filename(), dst_directory=folder)
927 self.folder_for_saving_episodes = folder
929 def copy_episode_bluetooth(self, url, *args):
930 episode = self.active_channel.find_episode(url)
931 filename = episode.local_filename()
933 if gl.config.bluetooth_use_device_address:
934 device = gl.config.bluetooth_device_address
935 else:
936 device = None
938 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
939 (base, ext) = os.path.splitext(filename)
940 if not destfile.endswith(ext):
941 destfile += ext
943 if gl.config.bluetooth_use_converter:
944 title = _('Converting file')
945 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
946 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
947 dlg.set_title(title)
948 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
949 dlg.show_all()
950 else:
951 dlg = None
953 def convert_and_send_thread(filename, destfile, device, dialog, notify):
954 if gl.config.bluetooth_use_converter:
955 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
956 result = p.wait()
957 if dialog is not None:
958 dialog.destroy()
959 else:
960 try:
961 shutil.copyfile(filename, destfile)
962 result = 0
963 except:
964 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
965 result = 1
967 if result == 0 or not os.path.exists(destfile):
968 util.bluetooth_send_file(destfile, device)
969 else:
970 notify(_('Error converting file.'), _('Bluetooth file transfer'))
971 util.delete_file(destfile)
973 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
975 def treeview_button_pressed( self, treeview, event):
976 global WEB_BROWSER_ICON
978 # Use right-click for the Desktop version and left-click for Maemo
979 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
980 (event.button == 3 and gpodder.interface == gpodder.GUI):
981 ( x, y ) = ( int(event.x), int(event.y) )
982 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
984 paths = []
986 # Did the user right-click into a selection?
987 selection = self.treeAvailable.get_selection()
988 if selection.count_selected_rows() and path:
989 ( model, paths ) = selection.get_selected_rows()
990 if path not in paths:
991 # We have right-clicked, but not into the
992 # selection, assume we don't want to operate
993 # on the selection
994 paths = []
996 # No selection or right click not in selection:
997 # Select the single item where we clicked
998 if not len( paths) and path:
999 treeview.grab_focus()
1000 treeview.set_cursor( path, column, 0)
1002 ( model, paths ) = ( treeview.get_model(), [ path ] )
1004 # We did not find a selection, and the user didn't
1005 # click on an item to select -- don't show the menu
1006 if not len( paths):
1007 return True
1009 first_url = model.get_value( model.get_iter( paths[0]), 0)
1010 episode = db.load_episode(first_url)
1012 menu = gtk.Menu()
1014 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1016 if can_play:
1017 if open_instead_of_play:
1018 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1019 else:
1020 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1021 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
1022 menu.append(self.set_finger_friendly(item))
1024 if not episode['is_locked'] and can_delete:
1025 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1026 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1027 menu.append(self.set_finger_friendly(item))
1029 if can_cancel:
1030 item = gtk.ImageMenuItem( _('Cancel download'))
1031 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
1032 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
1033 menu.append(self.set_finger_friendly(item))
1035 if can_download:
1036 item = gtk.ImageMenuItem(_('Download'))
1037 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1038 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
1039 menu.append(self.set_finger_friendly(item))
1041 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1042 item = gtk.ImageMenuItem(_('Do not download'))
1043 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1044 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1045 menu.append(self.set_finger_friendly(item))
1046 elif episode['state'] == db.STATE_NORMAL and can_download:
1047 item = gtk.ImageMenuItem(_('Mark as new'))
1048 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1049 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1050 menu.append(self.set_finger_friendly(item))
1052 if can_play and not can_download:
1053 menu.append( gtk.SeparatorMenuItem())
1054 item = gtk.ImageMenuItem(_('Save to disk'))
1055 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1056 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1057 menu.append(self.set_finger_friendly(item))
1058 if gl.bluetooth_available:
1059 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1060 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1061 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1062 menu.append(self.set_finger_friendly(item))
1063 if can_transfer:
1064 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1065 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1066 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1067 menu.append(self.set_finger_friendly(item))
1069 if can_play:
1070 menu.append( gtk.SeparatorMenuItem())
1071 is_played = episode['is_played']
1072 if is_played:
1073 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1074 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1075 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1076 menu.append(self.set_finger_friendly(item))
1077 else:
1078 item = gtk.ImageMenuItem(_('Mark as played'))
1079 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1080 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1081 menu.append(self.set_finger_friendly(item))
1083 is_locked = episode['is_locked']
1084 if is_locked:
1085 item = gtk.ImageMenuItem(_('Allow deletion'))
1086 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1087 item.connect('activate', self.on_item_toggle_lock_activate)
1088 menu.append(self.set_finger_friendly(item))
1089 else:
1090 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1091 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1092 item.connect('activate', self.on_item_toggle_lock_activate)
1093 menu.append(self.set_finger_friendly(item))
1095 if len(paths) == 1:
1096 menu.append(gtk.SeparatorMenuItem())
1097 # Single item, add episode information menu item
1098 episode_url = model.get_value( model.get_iter( paths[0]), 0)
1099 item = gtk.ImageMenuItem(_('Episode details'))
1100 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1101 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1102 menu.append(self.set_finger_friendly(item))
1103 episode = self.active_channel.find_episode(episode_url)
1104 # If we have it, also add episode website link
1105 if episode and episode.link and episode.link != episode.url:
1106 item = gtk.ImageMenuItem(_('Visit website'))
1107 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1108 item.connect('activate', lambda w: util.open_website(episode.link))
1109 menu.append(self.set_finger_friendly(item))
1111 if gpodder.interface == gpodder.MAEMO:
1112 # Because we open the popup on left-click for Maemo,
1113 # we also include a non-action to close the menu
1114 menu.append(gtk.SeparatorMenuItem())
1115 item = gtk.ImageMenuItem(_('Close this menu'))
1116 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1117 menu.append(self.set_finger_friendly(item))
1119 menu.show_all()
1120 # Disable tooltips while we are showing the menu, so
1121 # the tooltip will not appear over the menu
1122 self.episode_list_can_tooltip = False
1123 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1124 menu.popup( None, None, None, event.button, event.time)
1126 return True
1128 def set_title(self, new_title):
1129 self.default_title = new_title
1130 self.gPodder.set_title(new_title)
1132 def download_progress_updated( self, count, percentage):
1133 title = [ self.default_title ]
1135 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1137 if count == 1:
1138 title.append( _('downloading one file'))
1139 elif count > 1:
1140 title.append( _('downloading %d files') % count)
1142 if len(title) == 2:
1143 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1145 self.gPodder.set_title( ' - '.join( title))
1147 # Have all the downloads completed?
1148 # If so execute user command if defined, else do nothing
1149 if count == 0:
1150 if len(gl.config.cmd_all_downloads_complete) > 0:
1151 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1153 def update_selected_episode_list_icons(self):
1155 Updates the status icons in the episode list
1157 selection = self.treeAvailable.get_selection()
1158 (model, paths) = selection.get_selected_rows()
1159 for path in paths:
1160 iter = model.get_iter(path)
1161 self.active_channel.iter_set_downloading_columns(model, iter)
1163 def update_episode_list_icons(self, urls):
1165 Updates the status icons in the episode list
1166 Only update the episodes that have an URL in
1167 the "urls" iterable object (e.g. a list of URLs)
1169 if self.active_channel is None:
1170 return
1172 model = self.treeAvailable.get_model()
1173 if model is None:
1174 return
1176 for url in urls:
1177 if url in self.url_path_mapping:
1178 path = (self.url_path_mapping[url],)
1179 self.active_channel.iter_set_downloading_columns(model, model.get_iter(path))
1181 def playback_episode(self, episode, stream=False):
1182 (success, application) = gl.playback_episode(episode, stream)
1183 if not success:
1184 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), ))
1185 self.update_selected_episode_list_icons()
1186 self.updateComboBox(only_selected_channel=True)
1188 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1189 if model is None:
1190 return True
1192 key = key.lower()
1194 # columns, as defined in libpodcasts' get model method
1195 # 1 = episode title, 7 = description
1196 columns = (1, 7)
1198 for column in columns:
1199 value = model.get_value( iter, column).lower()
1200 if value.find( key) != -1:
1201 return False
1203 return True
1205 def change_menu_item(self, menuitem, icon=None, label=None):
1206 if icon is not None:
1207 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1208 if label is not None:
1209 label_widget = menuitem.get_child()
1210 label_widget.set_text(label)
1212 def play_or_download(self):
1213 if self.wNotebook.get_current_page() > 0:
1214 return
1216 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1217 ( is_played, is_locked ) = (False,)*2
1219 open_instead_of_play = False
1221 selection = self.treeAvailable.get_selection()
1222 if selection.count_selected_rows() > 0:
1223 (model, paths) = selection.get_selected_rows()
1225 for path in paths:
1226 url = model.get_value( model.get_iter( path), 0)
1227 local_filename = model.get_value( model.get_iter( path), 8)
1229 episode = podcastItem.load(url, self.active_channel)
1231 if episode.file_type() not in ('audio', 'video'):
1232 open_instead_of_play = True
1234 if episode.was_downloaded():
1235 can_play = episode.was_downloaded(and_exists=True)
1236 can_delete = True
1237 is_played = episode.is_played
1238 is_locked = episode.is_locked
1239 if not can_play:
1240 can_download = True
1241 else:
1242 if services.download_status_manager.is_download_in_progress(url):
1243 can_cancel = True
1244 else:
1245 can_download = True
1247 can_download = can_download and not can_cancel
1248 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1249 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1251 if open_instead_of_play:
1252 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1253 can_transfer = False
1254 else:
1255 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1257 self.toolPlay.set_sensitive( can_play)
1258 self.toolDownload.set_sensitive( can_download)
1259 self.toolTransfer.set_sensitive( can_transfer)
1260 self.toolCancel.set_sensitive( can_cancel)
1262 if can_cancel:
1263 self.item_cancel_download.show_all()
1264 else:
1265 self.item_cancel_download.hide_all()
1266 if can_download:
1267 self.itemDownloadSelected.show_all()
1268 else:
1269 self.itemDownloadSelected.hide_all()
1270 if can_play:
1271 if open_instead_of_play:
1272 self.itemOpenSelected.show_all()
1273 self.itemPlaySelected.hide_all()
1274 else:
1275 self.itemPlaySelected.show_all()
1276 self.itemOpenSelected.hide_all()
1277 if not can_download:
1278 self.itemDeleteSelected.show_all()
1279 else:
1280 self.itemDeleteSelected.hide_all()
1281 self.item_toggle_played.show_all()
1282 self.item_toggle_lock.show_all()
1283 self.separator9.show_all()
1284 if is_played:
1285 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1286 else:
1287 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1288 if is_locked:
1289 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1290 else:
1291 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1292 else:
1293 self.itemPlaySelected.hide_all()
1294 self.itemOpenSelected.hide_all()
1295 self.itemDeleteSelected.hide_all()
1296 self.item_toggle_played.hide_all()
1297 self.item_toggle_lock.hide_all()
1298 self.separator9.hide_all()
1299 if can_play or can_download or can_cancel:
1300 self.item_episode_details.show_all()
1301 self.separator16.show_all()
1302 self.no_episode_selected.hide_all()
1303 else:
1304 self.item_episode_details.hide_all()
1305 self.separator16.hide_all()
1306 self.no_episode_selected.show_all()
1308 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1310 def download_status_updated(self, episode_urls, channel_urls):
1311 count = services.download_status_manager.count()
1312 if count:
1313 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1314 else:
1315 self.labelDownloads.set_text( _('Downloads'))
1317 self.update_episode_list_icons(episode_urls)
1318 self.updateComboBox(only_these_urls=channel_urls)
1320 def on_cbMaxDownloads_toggled(self, widget, *args):
1321 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1323 def on_cbLimitDownloads_toggled(self, widget, *args):
1324 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1326 def episode_new_status_changed(self, urls):
1327 self.updateComboBox()
1328 self.update_episode_list_icons(urls)
1330 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1331 selection = self.treeChannels.get_selection()
1332 (model, iter) = selection.get_selected()
1334 if only_selected_channel:
1335 # very cheap! only update selected channel
1336 if iter and self.active_channel is not None:
1337 update_channel_model_by_iter(model, iter,
1338 self.active_channel, self.channel_colors,
1339 self.cover_cache,
1340 gl.config.podcast_list_icon_size,
1341 gl.config.podcast_list_icon_size)
1342 elif not self.channel_list_changed:
1343 # we can keep the model, but have to update some
1344 if only_these_urls is None:
1345 # still cheaper than reloading the whole list
1346 iter = model.get_iter_first()
1347 while iter is not None:
1348 (index,) = model.get_path(iter)
1349 update_channel_model_by_iter(model, iter,
1350 self.channels[index], self.channel_colors,
1351 self.cover_cache,
1352 gl.config.podcast_list_icon_size,
1353 gl.config.podcast_list_icon_size)
1354 iter = model.iter_next(iter)
1355 else:
1356 # ok, we got a bunch of urls to update
1357 for url in only_these_urls:
1358 if url in self.channel_url_path_mapping:
1359 index = self.channel_url_path_mapping[url]
1360 path = (index,)
1361 iter = model.get_iter(path)
1362 update_channel_model_by_iter(model, iter,
1363 self.channels[index], self.channel_colors,
1364 self.cover_cache,
1365 gl.config.podcast_list_icon_size,
1366 gl.config.podcast_list_icon_size)
1367 else:
1368 if model and iter and selected_url is None:
1369 # Get the URL of the currently-selected podcast
1370 selected_url = model.get_value(iter, 0)
1372 (model, urls) = channels_to_model(self.channels,
1373 self.channel_colors, self.cover_cache,
1374 gl.config.podcast_list_icon_size,
1375 gl.config.podcast_list_icon_size)
1377 self.channel_url_path_mapping = dict(zip(urls, range(len(urls))))
1378 self.treeChannels.set_model(model)
1380 try:
1381 selected_path = (0,)
1382 # Find the previously-selected URL in the new
1383 # model if we have an URL (else select first)
1384 if selected_url is not None:
1385 pos = model.get_iter_first()
1386 while pos is not None:
1387 url = model.get_value(pos, 0)
1388 if url == selected_url:
1389 selected_path = model.get_path(pos)
1390 break
1391 pos = model.iter_next(pos)
1393 self.treeChannels.get_selection().select_path(selected_path)
1394 except:
1395 log( 'Cannot set selection on treeChannels', sender = self)
1396 self.on_treeChannels_cursor_changed( self.treeChannels)
1397 self.channel_list_changed = False
1399 def updateTreeView(self):
1400 if self.channels and self.active_channel is not None:
1401 (model, urls) = self.active_channel.get_tree_model()
1402 self.treeAvailable.set_model(model)
1403 self.url_path_mapping = dict(zip(urls, range(len(urls))))
1404 self.treeAvailable.columns_autosize()
1405 self.play_or_download()
1406 else:
1407 model = self.treeAvailable.get_model()
1408 if model is not None:
1409 model.clear()
1411 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1412 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1414 dnd_channel = None
1415 if path is not None:
1416 model = self.treeChannels.get_model()
1417 iter = model.get_iter(path)
1418 url = model.get_value(iter, 0)
1419 for channel in self.channels:
1420 if channel.url == url:
1421 dnd_channel = channel
1422 break
1424 result = sel.data
1425 rl = result.strip().lower()
1426 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1427 services.cover_downloader.replace_cover(dnd_channel, result)
1428 else:
1429 self.add_new_channel(result)
1431 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1432 (scheme, rest) = result.split('://', 1)
1433 result = util.normalize_feed_url(result)
1435 if not result:
1436 cute_scheme = saxutils.escape(scheme)+'://'
1437 title = _('%s URLs are not supported') % cute_scheme
1438 message = _('gPodder does not understand the URL you supplied.')
1439 self.show_message( message, title)
1440 return
1442 for old_channel in self.channels:
1443 if old_channel.url == result:
1444 log( 'Channel already exists: %s', result)
1445 # Select the existing channel in combo box
1446 for i in range( len( self.channels)):
1447 if self.channels[i] == old_channel:
1448 self.treeChannels.get_selection().select_path( (i,))
1449 self.on_treeChannels_cursor_changed(self.treeChannels)
1450 break
1451 self.show_message( _('You have already subscribed to this podcast: %s') % (
1452 saxutils.escape( old_channel.title), ), _('Already added'))
1453 return
1455 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1456 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1457 waitdlg.set_title(_('Downloading episode list'))
1458 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1459 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1460 waitpb = gtk.ProgressBar()
1461 if block:
1462 waitdlg.vbox.add(waitpb)
1463 waitdlg.show_all()
1464 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1466 self.entryAddChannel.set_text(_('Downloading feed...'))
1467 self.entryAddChannel.set_sensitive(False)
1468 self.btnAddChannel.set_sensitive(False)
1469 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1470 thread = Thread( target=self.add_new_channel_proc, args=args )
1471 thread.start()
1473 while block and thread.isAlive():
1474 while gtk.events_pending():
1475 gtk.main_iteration( False)
1476 waitpb.pulse()
1477 time.sleep(0.05)
1480 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1481 log( 'Adding new channel: %s', url)
1482 channel = error = None
1483 try:
1484 channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1485 except HTTPAuthError, e:
1486 error = e
1487 except Exception, e:
1488 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1490 util.idle_add( callback, channel, url, error, *callback_args )
1492 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1493 if channel is not None:
1494 self.channels.append( channel)
1495 self.channel_list_changed = True
1496 save_channels( self.channels)
1497 if not quiet:
1498 # download changed channels and select the new episode in the UI afterwards
1499 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1501 (username, password) = util.username_password_from_url( url)
1502 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')):
1503 channel.username = username
1504 channel.password = password
1505 log('Saving authentication data for episode downloads..', sender = self)
1506 channel.save()
1507 # We need to update the channel list otherwise the authentication
1508 # data won't show up in the channel editor.
1509 # TODO: Only updated the newly added feed to save some cpu cycles
1510 self.channels = load_channels()
1511 self.channel_list_changed = True
1513 if ask_download_new:
1514 new_episodes = channel.get_new_episodes()
1515 if len(new_episodes):
1516 self.new_episodes_show(new_episodes)
1518 elif isinstance( error, HTTPAuthError ):
1519 response, auth_tokens = self.UsernamePasswordDialog(
1520 _('Feed requires authentication'), _('Please enter your username and password.'))
1522 if response:
1523 self.add_new_channel( url, authentication_tokens=auth_tokens )
1525 else:
1526 # Ok, the URL is not a channel, or there is some other
1527 # error - let's see if it's a web page or OPML file...
1528 try:
1529 data = urllib2.urlopen(url).read().lower()
1530 if '</opml>' in data:
1531 # This looks like an OPML feed
1532 self.on_item_import_from_file_activate(None, url)
1534 elif '</html>' in data:
1535 # This looks like a web page
1536 title = _('The URL is a website')
1537 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.)')
1538 if self.show_confirmation(message, title):
1539 util.open_website(url)
1541 except Exception, e:
1542 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1544 title = _('Error adding podcast')
1545 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1546 self.show_message( message, title)
1548 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1549 self.entryAddChannel.set_sensitive(True)
1550 self.btnAddChannel.set_sensitive(True)
1551 self.update_podcasts_tab()
1552 waitdlg.destroy()
1555 def update_feed_cache_finish_callback(self, channels=None,
1556 notify_no_new_episodes=False, select_url_afterwards=None):
1558 db.commit()
1560 self.updating_feed_cache = False
1561 self.hboxUpdateFeeds.hide_all()
1562 self.btnUpdateFeeds.show_all()
1563 self.itemUpdate.set_sensitive(True)
1564 self.itemUpdateChannel.set_sensitive(True)
1566 # If we want to select a specific podcast (via its URL)
1567 # after the update, we give it to updateComboBox here to
1568 # select exactly this podcast after updating the view
1569 self.updateComboBox(selected_url=select_url_afterwards)
1571 if self.tray_icon:
1572 self.tray_icon.set_status(None)
1573 if self.minimized:
1574 new_episodes = []
1575 # look for new episodes to notify
1576 for channel in self.channels:
1577 for episode in channel.get_new_episodes():
1578 if not episode in self.already_notified_new_episodes:
1579 new_episodes.append(episode)
1580 self.already_notified_new_episodes.append(episode)
1581 # notify new episodes
1583 if len(new_episodes) == 0:
1584 if notify_no_new_episodes and self.tray_icon is not None:
1585 msg = _('No new episodes available for download')
1586 self.tray_icon.send_notification(msg)
1587 return
1588 elif len(new_episodes) == 1:
1589 title = _('gPodder has found %s') % (_('one new episode:'),)
1590 else:
1591 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1592 message = self.tray_icon.format_episode_list(new_episodes)
1594 #auto download new episodes
1595 if gl.config.auto_download_when_minimized:
1596 message += '\n<i>(%s...)</i>' % _('downloading')
1597 self.download_episode_list(new_episodes)
1598 self.tray_icon.send_notification(message, title)
1599 return
1601 # open the episodes selection dialog
1602 self.channels = load_channels()
1603 self.channel_list_changed = True
1604 self.updateComboBox()
1605 if not self.feed_cache_update_cancelled:
1606 self.download_all_new(channels=channels)
1608 def update_feed_cache_callback(self, progressbar, title, position, count):
1609 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1610 progressbar.set_text(progression)
1611 if self.tray_icon:
1612 self.tray_icon.set_status(
1613 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1614 if count > 0:
1615 progressbar.set_fraction(float(position)/float(count))
1617 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1618 callback_proc, finish_proc):
1620 semaphore.acquire()
1621 if not self.feed_cache_update_cancelled:
1622 try:
1623 channel.update()
1624 except:
1625 log('Darn SQLite LOCK!', sender=self, traceback=True)
1627 # By the time we get here the update may have already been cancelled
1628 if not self.feed_cache_update_cancelled:
1629 callback_proc(channel.title, self.updated_feeds, total_channels)
1631 self.updated_feeds += 1
1632 self.treeview_channel_set_color( channel, 'default' )
1633 channel.update_flag = False
1635 semaphore.release()
1636 if self.updated_feeds == total_channels:
1637 finish_proc()
1639 def on_btnCancelFeedUpdate_clicked(self, widget):
1640 self.pbFeedUpdate.set_text(_('Cancelling...'))
1641 self.feed_cache_update_cancelled = True
1643 def update_feed_cache(self, channels=None, force_update=True,
1644 notify_no_new_episodes=False, select_url_afterwards=None):
1646 if self.updating_feed_cache:
1647 return
1649 if not force_update:
1650 self.channels = load_channels()
1651 self.channel_list_changed = True
1652 self.updateComboBox()
1653 return
1655 self.updating_feed_cache = True
1656 self.itemUpdate.set_sensitive(False)
1657 self.itemUpdateChannel.set_sensitive(False)
1659 if self.tray_icon:
1660 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1662 if channels is None:
1663 channels = self.channels
1665 if len(channels) == 1:
1666 text = _('Updating %d feed.')
1667 else:
1668 text = _('Updating %d feeds.')
1669 self.pbFeedUpdate.set_text( text % len(channels))
1670 self.pbFeedUpdate.set_fraction(0)
1672 # let's get down to business..
1673 callback_proc = lambda title, pos, count: util.idle_add(
1674 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1675 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1676 channels, notify_no_new_episodes, select_url_afterwards )
1678 self.updated_feeds = 0
1679 self.feed_cache_update_cancelled = False
1680 self.btnUpdateFeeds.hide_all()
1681 self.hboxUpdateFeeds.show_all()
1682 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1684 for channel in channels:
1685 self.treeview_channel_set_color( channel, 'updating' )
1686 channel.update_flag = True
1687 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1688 thread = Thread( target = self.update_feed_cache_proc, args = args)
1689 thread.start()
1691 def treeview_channel_set_color( self, channel, color ):
1692 if self.treeChannels.get_model():
1693 if color in self.channel_colors:
1694 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1695 else:
1696 self.treeChannels.get_model().set(channel.iter, 8, color)
1698 def on_gPodder_delete_event(self, widget, *args):
1699 """Called when the GUI wants to close the window
1700 Displays a confirmation dialog (and closes/hides gPodder)
1703 downloading = services.download_status_manager.has_items()
1705 # Only iconify if we are using the window's "X" button,
1706 # but not when we are using "Quit" in the menu or toolbar
1707 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1708 self.iconify_main_window()
1709 elif gl.config.on_quit_ask or downloading:
1710 if gpodder.interface == gpodder.MAEMO:
1711 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1712 if result:
1713 self.close_gpodder()
1714 else:
1715 return True
1716 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1717 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1718 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1720 title = _('Quit gPodder')
1721 if downloading:
1722 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1723 else:
1724 message = _('Do you really want to quit gPodder now?')
1726 dialog.set_title(title)
1727 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1728 if not downloading:
1729 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1730 dialog.vbox.pack_start(cb_ask)
1731 cb_ask.show_all()
1733 result = dialog.run()
1734 dialog.destroy()
1736 if result == gtk.RESPONSE_CLOSE:
1737 if not downloading and cb_ask.get_active() == True:
1738 gl.config.on_quit_ask = False
1739 self.close_gpodder()
1740 else:
1741 self.close_gpodder()
1743 return True
1745 def close_gpodder(self):
1746 """ clean everything and exit properly
1748 if self.channels:
1749 if save_channels(self.channels):
1750 if gl.config.my_gpodder_autoupload:
1751 log('Uploading to my.gpodder.org on close', sender=self)
1752 util.idle_add(self.on_upload_to_mygpo, None)
1753 else:
1754 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1756 services.download_status_manager.cancel_all()
1757 self.gPodder.hide()
1758 while gtk.events_pending():
1759 gtk.main_iteration(False)
1761 db.close()
1763 self.gtk_main_quit()
1764 sys.exit( 0)
1766 def get_old_episodes(self):
1767 episodes = []
1768 for channel in self.channels:
1769 for episode in channel.get_downloaded_episodes():
1770 if episode.is_old() and not episode.is_locked and episode.is_played:
1771 episodes.append(episode)
1772 return episodes
1774 def for_each_selected_episode_url( self, callback):
1775 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1776 for path in paths:
1777 url = model.get_value( model.get_iter( path), 0)
1778 try:
1779 callback( url)
1780 except Exception, e:
1781 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1783 self.update_selected_episode_list_icons()
1784 self.updateComboBox(only_selected_channel=True)
1786 def delete_episode_list( self, episodes, confirm = True):
1787 if len(episodes) == 0:
1788 return
1790 if len(episodes) == 1:
1791 message = _('Do you really want to delete this episode?')
1792 else:
1793 message = _('Do you really want to delete %d episodes?') % len(episodes)
1795 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1796 return
1798 episode_urls = set()
1799 channel_urls = set()
1800 for episode in episodes:
1801 log('Deleting episode: %s', episode.title, sender = self)
1802 episode.delete_from_disk()
1803 episode_urls.add(episode.url)
1804 channel_urls.add(episode.channel.url)
1806 self.download_status_updated(episode_urls, channel_urls)
1808 def on_itemRemoveOldEpisodes_activate( self, widget):
1809 columns = (
1810 ('title_and_description', None, None, _('Episode')),
1811 ('channel_prop', None, None, _('Podcast')),
1812 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1813 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1814 ('played_prop', None, None, _('Status')),
1815 ('age_prop', None, None, _('Downloaded')),
1818 selection_buttons = {
1819 _('Select played'): lambda episode: episode.is_played,
1820 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1823 instructions = _('Select the episodes you want to delete from your hard disk.')
1825 episodes = []
1826 selected = []
1827 for channel in self.channels:
1828 for episode in channel.get_downloaded_episodes():
1829 if not episode.is_locked:
1830 episodes.append(episode)
1831 selected.append(episode.is_played)
1833 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1834 episodes = episodes, selected = selected, columns = columns, \
1835 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1836 selection_buttons = selection_buttons)
1838 def mark_selected_episodes_new(self):
1839 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1840 self.for_each_selected_episode_url(callback)
1842 def mark_selected_episodes_old(self):
1843 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1844 self.for_each_selected_episode_url(callback)
1846 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1847 if toggle:
1848 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1849 else:
1850 callback = lambda url: db.mark_episode(url, is_played=new_value)
1852 self.for_each_selected_episode_url(callback)
1854 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1855 if toggle:
1856 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1857 else:
1858 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1860 self.for_each_selected_episode_url(callback)
1862 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1863 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
1864 db.update_channel_lock(self.active_channel)
1866 if self.active_channel.channel_is_locked:
1867 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
1868 else:
1869 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
1871 for episode in self.active_channel.get_all_episodes():
1872 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
1874 self.updateComboBox(only_selected_channel=True)
1876 def on_item_email_subscriptions_activate(self, widget):
1877 if not self.channels:
1878 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1879 elif not gl.send_subscriptions():
1880 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1882 def on_itemUpdateChannel_activate(self, widget=None):
1883 self.update_feed_cache(channels=[self.active_channel,])
1885 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1886 restore_from = can_restore_from_opml()
1888 if self.channels:
1889 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1890 elif restore_from is not None:
1891 title = _('Database upgrade required')
1892 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?')
1893 if self.show_confirmation(message, title):
1894 add_callback = lambda url: self.add_new_channel(url, False, True)
1895 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1896 w.set_has_separator(False)
1897 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1898 w.set_default_size(500, -1)
1899 pb = gtk.ProgressBar()
1900 l = gtk.Label()
1901 l.set_padding(6, 3)
1902 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1903 l.set_alignment(0.0, 0.5)
1904 w.vbox.pack_start(l)
1905 l = gtk.Label()
1906 l.set_padding(6, 3)
1907 l.set_alignment(0.0, 0.5)
1908 l.set_text(_('Please wait while your settings are converted.'))
1909 w.vbox.pack_start(l)
1910 w.vbox.pack_start(pb)
1911 lb = gtk.Label()
1912 lb.set_ellipsize(pango.ELLIPSIZE_END)
1913 lb.set_alignment(0.0, 0.5)
1914 lb.set_padding(6, 6)
1915 w.vbox.pack_start(lb)
1917 def set_pb_status(pb, lb, fraction, text):
1918 pb.set_fraction(float(fraction)/100.0)
1919 pb.set_text('%.0f %%' % fraction)
1920 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1921 while gtk.events_pending():
1922 gtk.main_iteration(False)
1923 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1924 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1925 w.show_all()
1926 start = datetime.datetime.now()
1927 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1928 # Refresh the view with the updated episodes
1929 self.updateComboBox()
1930 time_taken = str(datetime.datetime.now()-start)
1931 status_callback(100.0, _('Migration finished in %s') % time_taken)
1932 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1933 w.run()
1934 w.destroy()
1935 else:
1936 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
1938 def download_episode_list(self, episodes):
1939 services.download_status_manager.start_batch_mode()
1940 for episode in episodes:
1941 log('Downloading episode: %s', episode.title, sender = self)
1942 filename = episode.local_filename()
1943 if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
1944 download.DownloadThread(episode.channel, episode, self.notification).start()
1945 services.download_status_manager.end_batch_mode()
1947 def new_episodes_show(self, episodes):
1948 columns = (
1949 ('title_and_description', None, None, _('Episode')),
1950 ('channel_prop', None, None, _('Podcast')),
1951 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1952 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1955 if len(episodes) > 0:
1956 instructions = _('Select the episodes you want to download now.')
1958 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1959 episodes=episodes, columns=columns, selected_default=True, \
1960 stock_ok_button = 'gpodder-download', \
1961 callback=self.download_episode_list, \
1962 remove_callback=lambda e: e.mark_old(), \
1963 remove_action=_('Never download'), \
1964 remove_finished=self.episode_new_status_changed)
1965 else:
1966 title = _('No new episodes')
1967 message = _('No new episodes to download.\nPlease check for new episodes later.')
1968 self.show_message(message, title)
1970 def on_itemDownloadAllNew_activate(self, widget, *args):
1971 self.download_all_new()
1973 def download_all_new(self, channels=None):
1974 if channels is None:
1975 channels = self.channels
1976 episodes = []
1977 for channel in channels:
1978 for episode in channel.get_new_episodes():
1979 episodes.append(episode)
1980 self.new_episodes_show(episodes)
1982 def get_all_episodes(self, exclude_nonsignificant=True ):
1983 """'exclude_nonsignificant' will exclude non-downloaded episodes
1984 and all episodes from channels that are set to skip when syncing"""
1985 episode_list = []
1986 for channel in self.channels:
1987 if not channel.sync_to_devices and exclude_nonsignificant:
1988 log('Skipping channel: %s', channel.title, sender=self)
1989 continue
1990 for episode in channel.get_all_episodes():
1991 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1992 episode_list.append(episode)
1993 return episode_list
1995 def ipod_delete_played(self, device):
1996 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1997 episodes_on_device = device.get_all_tracks()
1998 for local_episode in all_episodes:
1999 device_episode = device.episode_on_device(local_episode)
2000 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2001 or local_episode.state == db.STATE_DELETED ):
2002 log("mp3_player_delete_played: removing %s" % device_episode.title)
2003 device.remove_track(device_episode)
2005 def on_sync_to_ipod_activate(self, widget, episodes=None):
2006 # make sure gpod is available before even trying to sync
2007 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2008 title = _('Cannot Sync To iPod')
2009 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2010 self.notification( message, title )
2011 return
2012 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2013 title = _('Cannot sync to MTP device')
2014 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2015 self.notification( message, title )
2016 return
2018 device = sync.open_device()
2019 device.register( 'post-done', self.sync_to_ipod_completed )
2021 if device is None:
2022 title = _('No device configured')
2023 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2024 self.notification(message, title)
2025 return
2027 if not device.open():
2028 title = _('Cannot open device')
2029 message = _('There has been an error opening your device.')
2030 self.notification(message, title)
2031 return
2033 if gl.config.ipod_purge_old_episodes:
2034 device.purge()
2036 sync_all_episodes = not bool(episodes)
2038 if episodes is None:
2039 episodes = self.get_all_episodes()
2041 # make sure we have enough space on the device
2042 total_size = 0
2043 free_space = device.get_free_space()
2044 for episode in episodes:
2045 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
2046 total_size += util.calculate_size(str(episode.local_filename()))
2048 if total_size > free_space:
2049 # can be negative because of the 10 MiB for reserved for the iTunesDB
2050 free_space = max( free_space, 0 )
2051 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
2052 title = _('Not enough space left on device.')
2053 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
2054 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
2055 self.notification(message, title)
2056 else:
2057 # start syncing!
2058 gPodderSync(device=device, gPodder=self)
2059 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2060 if self.tray_icon:
2061 self.tray_icon.set_synchronisation_device(device)
2063 def sync_to_ipod_completed(self, device, successful_sync):
2064 device.unregister( 'post-done', self.sync_to_ipod_completed )
2066 if self.tray_icon:
2067 self.tray_icon.release_synchronisation_device()
2069 if not successful_sync:
2070 title = _('Error closing device')
2071 message = _('There has been an error closing your device.')
2072 self.notification(message, title)
2074 # update model for played state updates after sync
2075 util.idle_add(self.updateComboBox)
2077 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2078 if sync_all_episodes:
2079 device.add_tracks(episodes)
2080 # 'only_sync_not_played' must be used or else all the played
2081 # tracks will be copied then immediately deleted
2082 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2083 self.ipod_delete_played(device)
2084 else:
2085 device.add_tracks(episodes, force_played=True)
2086 device.close()
2088 def ipod_cleanup_callback(self, device, tracks):
2089 title = _('Delete podcasts from device?')
2090 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?')
2091 if len(tracks) > 0 and self.show_confirmation(message, title):
2092 device.remove_tracks(tracks)
2094 if not device.close():
2095 title = _('Error closing device')
2096 message = _('There has been an error closing your device.')
2097 self.show_message(message, title)
2098 return
2100 def on_cleanup_ipod_activate(self, widget, *args):
2101 columns = (
2102 ('title', None, None, _('Episode')),
2103 ('podcast', None, None, _('Podcast')),
2104 ('filesize', None, None, _('Size')),
2105 ('modified', None, None, _('Copied')),
2106 ('playcount', None, None, _('Play count')),
2107 ('released', None, None, _('Released')),
2110 device = sync.open_device()
2112 if device is None:
2113 title = _('No device configured')
2114 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2115 self.show_message(message, title)
2116 return
2118 if not device.open():
2119 title = _('Cannot open device')
2120 message = _('There has been an error opening your device.')
2121 self.show_message(message, title)
2122 return
2124 gPodderSync(device=device, gPodder=self)
2126 tracks = device.get_all_tracks()
2127 if len(tracks) > 0:
2128 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2129 wanted_columns = []
2130 for key, sort_name, sort_type, caption in columns:
2131 want_this_column = False
2132 for track in tracks:
2133 if getattr(track, key) is not None:
2134 want_this_column = True
2135 break
2137 if want_this_column:
2138 wanted_columns.append((key, sort_name, sort_type, caption))
2139 title = _('Remove podcasts from device')
2140 instructions = _('Select the podcast episodes you want to remove from your device.')
2141 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2142 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2143 else:
2144 title = _('No files on device')
2145 message = _('The devices contains no files to be removed.')
2146 self.show_message(message, title)
2147 device.close()
2149 def on_manage_device_playlist(self, widget):
2150 # make sure gpod is available before even trying to sync
2151 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2152 title = _('Cannot manage iPod playlist')
2153 message = _('This feature is not available for iPods.')
2154 self.notification( message, title )
2155 return
2156 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2157 title = _('Cannot manage MTP device playlist')
2158 message = _('This feature is not available for MTP devices.')
2159 self.notification( message, title )
2160 return
2162 device = sync.open_device()
2164 if device is None:
2165 title = _('No device configured')
2166 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2167 self.notification(message, title)
2168 return
2170 if not device.open():
2171 title = _('Cannot open device')
2172 message = _('There has been an error opening your device.')
2173 self.notification(message, title)
2174 return
2176 gPodderPlaylist(device=device, gPodder=self)
2177 device.close()
2179 def show_hide_tray_icon(self):
2180 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2181 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
2182 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2183 self.tray_icon.set_visible(False)
2184 del self.tray_icon
2185 self.tray_icon = None
2187 if gl.config.minimize_to_tray and self.tray_icon:
2188 self.tray_icon.set_visible(self.minimized)
2189 elif self.tray_icon:
2190 self.tray_icon.set_visible(True)
2192 def on_itemShowToolbar_activate(self, widget):
2193 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2195 def on_itemShowDescription_activate(self, widget):
2196 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2198 def update_item_device( self):
2199 if gl.config.device_type != 'none':
2200 self.itemDevice.show_all()
2201 (label,) = self.itemDevice.get_children()
2202 label.set_text(gl.get_device_name())
2203 else:
2204 self.itemDevice.hide_all()
2206 def properties_closed( self):
2207 self.show_hide_tray_icon()
2208 self.update_item_device()
2209 self.updateComboBox()
2211 def on_itemPreferences_activate(self, widget, *args):
2212 if gpodder.interface == gpodder.GUI:
2213 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2214 else:
2215 gPodderMaemoPreferences()
2217 def on_itemDependencies_activate(self, widget):
2218 gPodderDependencyManager()
2220 def on_add_new_google_search(self, widget, *args):
2221 def add_google_video_search(query):
2222 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2224 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2226 def on_upgrade_from_videocenter(self, widget):
2227 from gpodder import nokiavideocenter
2228 vc = nokiavideocenter.UpgradeFromVideocenter()
2229 if vc.db2opml():
2230 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))
2231 else:
2232 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2234 def require_my_gpodder_authentication(self):
2235 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2236 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'))
2237 if success and authentication[0] and authentication[1]:
2238 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2239 return True
2240 else:
2241 return False
2243 return True
2245 def my_gpodder_offer_autoupload(self):
2246 if not gl.config.my_gpodder_autoupload:
2247 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')):
2248 gl.config.my_gpodder_autoupload = True
2250 def on_download_from_mygpo(self, widget):
2251 if self.require_my_gpodder_authentication():
2252 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2253 opml_data = client.download_subscriptions()
2254 if len(opml_data) > 0:
2255 fp = open(gl.channel_opml_file, 'w')
2256 fp.write(opml_data)
2257 fp.close()
2258 (added, skipped) = (0, 0)
2259 i = opml.Importer(gl.channel_opml_file)
2260 for item in i.items:
2261 url = item['url']
2262 if url not in (c.url for c in self.channels):
2263 self.add_new_channel(url, ask_download_new=False, block=True)
2264 added += 1
2265 else:
2266 log('Already added: %s', url, sender=self)
2267 skipped += 1
2268 self.updateComboBox()
2269 if added > 0:
2270 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2271 elif widget is not None:
2272 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2273 self.my_gpodder_offer_autoupload()
2274 else:
2275 gl.config.my_gpodder_password = ''
2276 self.on_download_from_mygpo(widget)
2277 else:
2278 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2280 def on_upload_to_mygpo(self, widget):
2281 if self.require_my_gpodder_authentication():
2282 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2283 save_channels(self.channels)
2284 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2285 if widget is not None:
2286 self.show_message('\n'.join(messages), _('Results of upload'))
2287 if not success:
2288 gl.config.my_gpodder_password = ''
2289 self.on_upload_to_mygpo(widget)
2290 else:
2291 self.my_gpodder_offer_autoupload()
2292 elif not success:
2293 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2294 elif widget is not None:
2295 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2297 def on_itemAddChannel_activate(self, widget, *args):
2298 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2300 def on_itemEditChannel_activate(self, widget, *args):
2301 if self.active_channel is None:
2302 title = _('No podcast selected')
2303 message = _('Please select a podcast in the podcasts list to edit.')
2304 self.show_message( message, title)
2305 return
2307 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2309 def change_channel_url(self, old_url, new_url):
2310 channel = None
2311 try:
2312 channel = podcastChannel.load(url=new_url, create=True)
2313 except:
2314 channel = None
2316 if channel is None:
2317 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2318 return
2320 for channel in self.channels:
2321 if channel.url == old_url:
2322 log('=> change channel url from %s to %s', old_url, new_url)
2323 old_save_dir = channel.save_dir
2324 channel.url = new_url
2325 new_save_dir = channel.save_dir
2326 log('old save dir=%s', old_save_dir, sender=self)
2327 log('new save dir=%s', new_save_dir, sender=self)
2328 files = glob.glob(os.path.join(old_save_dir, '*'))
2329 log('moving %d files to %s', len(files), new_save_dir, sender=self)
2330 for file in files:
2331 log('moving %s', file, sender=self)
2332 shutil.move(file, new_save_dir)
2333 try:
2334 os.rmdir(old_save_dir)
2335 except:
2336 log('Warning: cannot delete %s', old_save_dir, sender=self)
2338 save_channels(self.channels)
2339 # update feed cache and select the podcast with the new URL afterwards
2340 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2342 def on_itemRemoveChannel_activate(self, widget, *args):
2343 try:
2344 if gpodder.interface == gpodder.GUI:
2345 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2346 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2347 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2349 title = _('Remove podcast and episodes?')
2350 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2352 dialog.set_title(title)
2353 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2355 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2356 dialog.vbox.pack_start(cb_ask)
2357 cb_ask.show_all()
2358 affirmative = gtk.RESPONSE_YES
2359 elif gpodder.interface == gpodder.MAEMO:
2360 cb_ask = gtk.CheckButton('') # dummy check button
2361 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2362 affirmative = gtk.RESPONSE_OK
2364 result = dialog.run()
2365 dialog.destroy()
2367 if result == affirmative:
2368 # delete downloaded episodes only if checkbox is unchecked
2369 if cb_ask.get_active() == False:
2370 self.active_channel.remove_downloaded()
2371 else:
2372 log('Not removing downloaded episodes', sender=self)
2374 # only delete partial files if we do not have any downloads in progress
2375 delete_partial = not services.download_status_manager.has_items()
2376 gl.clean_up_downloads(delete_partial)
2378 # cancel any active downloads from this channel
2379 if not delete_partial:
2380 for episode in self.active_channel.get_all_episodes():
2381 services.download_status_manager.cancel_by_url(episode.url)
2383 # get the URL of the podcast we want to select next
2384 position = self.channels.index(self.active_channel)
2385 if position == len(self.channels)-1:
2386 # this is the last podcast, so select the URL
2387 # of the item before this one (i.e. the "new last")
2388 select_url = self.channels[position-1].url
2389 else:
2390 # there is a podcast after the deleted one, so
2391 # we simply select the one that comes after it
2392 select_url = self.channels[position+1].url
2394 # Remove the channel
2395 self.active_channel.delete()
2396 self.channels.remove(self.active_channel)
2397 self.channel_list_changed = True
2398 save_channels(self.channels)
2400 # Re-load the channels and select the desired new channel
2401 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2402 except:
2403 log('There has been an error removing the channel.', traceback=True, sender=self)
2404 self.update_podcasts_tab()
2406 def get_opml_filter(self):
2407 filter = gtk.FileFilter()
2408 filter.add_pattern('*.opml')
2409 filter.add_pattern('*.xml')
2410 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2411 return filter
2413 def on_item_import_from_file_activate(self, widget, filename=None):
2414 if filename is None:
2415 if gpodder.interface == gpodder.GUI:
2416 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2417 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2418 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2419 elif gpodder.interface == gpodder.MAEMO:
2420 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2421 dlg.set_filter(self.get_opml_filter())
2422 response = dlg.run()
2423 filename = None
2424 if response == gtk.RESPONSE_OK:
2425 filename = dlg.get_filename()
2426 dlg.destroy()
2428 if filename is not None:
2429 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))
2431 def on_itemExportChannels_activate(self, widget, *args):
2432 if not self.channels:
2433 title = _('Nothing to export')
2434 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2435 self.show_message( message, title)
2436 return
2438 if gpodder.interface == gpodder.GUI:
2439 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2440 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2441 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2442 elif gpodder.interface == gpodder.MAEMO:
2443 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2444 dlg.set_filter(self.get_opml_filter())
2445 response = dlg.run()
2446 if response == gtk.RESPONSE_OK:
2447 filename = dlg.get_filename()
2448 dlg.destroy()
2449 exporter = opml.Exporter( filename)
2450 if exporter.write(self.channels):
2451 if len(self.channels) == 1:
2452 title = _('One subscription exported')
2453 else:
2454 title = _('%d subscriptions exported') % len(self.channels)
2455 self.show_message(_('Your podcast list has been successfully exported.'), title)
2456 else:
2457 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2458 else:
2459 dlg.destroy()
2461 def on_itemImportChannels_activate(self, widget, *args):
2462 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))
2464 def on_homepage_activate(self, widget, *args):
2465 util.open_website(app_website)
2467 def on_wiki_activate(self, widget, *args):
2468 util.open_website('http://wiki.gpodder.org/')
2470 def on_bug_tracker_activate(self, widget, *args):
2471 util.open_website('http://bugs.gpodder.org/')
2473 def on_itemAbout_activate(self, widget, *args):
2474 dlg = gtk.AboutDialog()
2475 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2476 dlg.set_version( app_version)
2477 dlg.set_copyright( app_copyright)
2478 dlg.set_website( app_website)
2479 dlg.set_translator_credits( _('translator-credits'))
2480 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2482 if gpodder.interface == gpodder.GUI:
2483 # For the "GUI" version, we add some more
2484 # items to the about dialog (credits and logo)
2485 dlg.set_authors(app_authors)
2486 try:
2487 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2488 except:
2489 pass
2491 dlg.run()
2493 def on_wNotebook_switch_page(self, widget, *args):
2494 page_num = args[1]
2495 if gpodder.interface == gpodder.MAEMO:
2496 page = self.wNotebook.get_nth_page(page_num)
2497 tab_label = self.wNotebook.get_tab_label(page).get_text()
2498 if page_num == 0 and self.active_channel is not None:
2499 self.set_title(self.active_channel.title)
2500 else:
2501 self.set_title(tab_label)
2502 if page_num == 0:
2503 self.play_or_download()
2504 else:
2505 self.toolDownload.set_sensitive( False)
2506 self.toolPlay.set_sensitive( False)
2507 self.toolTransfer.set_sensitive( False)
2508 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2510 def on_treeChannels_row_activated(self, widget, *args):
2511 # double-click action of the podcast list
2512 pass
2514 def on_treeChannels_cursor_changed(self, widget, *args):
2515 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2517 if model is not None and iter is not None:
2518 old_active_channel = self.active_channel
2519 (id,) = model.get_path(iter)
2520 self.active_channel = self.channels[id]
2522 if self.active_channel == old_active_channel:
2523 return
2525 if gpodder.interface == gpodder.MAEMO:
2526 self.set_title(self.active_channel.title)
2527 self.itemEditChannel.show_all()
2528 self.itemRemoveChannel.show_all()
2529 self.channel_toggle_lock.show_all()
2530 if self.active_channel.channel_is_locked:
2531 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2532 else:
2533 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2535 else:
2536 self.active_channel = None
2537 self.itemEditChannel.hide_all()
2538 self.itemRemoveChannel.hide_all()
2539 self.channel_toggle_lock.hide_all()
2541 self.updateTreeView()
2543 def on_entryAddChannel_changed(self, widget, *args):
2544 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2545 self.btnAddChannel.set_sensitive( active)
2547 def on_btnAddChannel_clicked(self, widget, *args):
2548 url = self.entryAddChannel.get_text()
2549 self.entryAddChannel.set_text('')
2550 self.add_new_channel( url)
2552 def on_btnEditChannel_clicked(self, widget, *args):
2553 self.on_itemEditChannel_activate( widget, args)
2555 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2557 What this function does depends on from which widget it is called.
2558 It gets the selected episodes of the current podcast and runs one
2559 of the following actions on them:
2561 * Transfer (to MP3 player, iPod, etc..)
2562 * Playback/open files
2563 * Show the episode info dialog
2564 * Download episodes
2566 try:
2567 selection = self.treeAvailable.get_selection()
2568 (model, paths) = selection.get_selected_rows()
2570 if len(paths) == 0:
2571 log('Nothing selected', sender=self)
2572 return
2574 wname = widget.get_name()
2575 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2576 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2577 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2579 episodes = []
2580 for path in paths:
2581 it = model.get_iter(path)
2582 url = model.get_value(it, 0)
2583 episode = self.active_channel.find_episode(url)
2584 episodes.append(episode)
2586 if len(episodes) == 0:
2587 log('No episodes selected', sender=self)
2589 if do_transfer:
2590 self.on_sync_to_ipod_activate(widget, episodes)
2591 elif do_playback:
2592 for episode in episodes:
2593 # Make sure to mark the episode as downloaded
2594 if os.path.exists(episode.local_filename()):
2595 episode.channel.addDownloadedItem(episode)
2596 self.playback_episode(episode)
2597 elif gl.config.enable_streaming:
2598 self.playback_episode(episode, stream=True)
2599 elif do_epdialog:
2600 play_callback = lambda: self.playback_episode(episode)
2601 def download_callback():
2602 self.download_episode_list([episode])
2603 self.play_or_download()
2604 if self.gpodder_episode_window is None:
2605 log('First-time use of episode window --- creating', sender=self)
2606 self.gpodder_episode_window = gPodderEpisode()
2607 self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
2608 else:
2609 self.download_episode_list(episodes)
2610 self.play_or_download()
2611 except:
2612 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2614 def on_treeAvailable_button_release_event(self, widget, *args):
2615 self.play_or_download()
2617 def auto_update_procedure(self, first_run=False):
2618 log('auto_update_procedure() got called', sender=self)
2619 if not first_run and gl.config.auto_update_feeds and self.minimized:
2620 self.update_feed_cache(force_update=True)
2622 next_update = 60*1000*gl.config.auto_update_frequency
2623 gobject.timeout_add(next_update, self.auto_update_procedure)
2625 def on_treeDownloads_row_activated(self, widget, *args):
2626 cancel_urls = []
2628 if self.wNotebook.get_current_page() > 0:
2629 # Use the download list treeview + model
2630 ( tree, column ) = ( self.treeDownloads, 3 )
2631 else:
2632 # Use the available podcasts treeview + model
2633 ( tree, column ) = ( self.treeAvailable, 0 )
2635 selection = tree.get_selection()
2636 (model, paths) = selection.get_selected_rows()
2637 for path in paths:
2638 url = model.get_value( model.get_iter( path), column)
2639 cancel_urls.append( url)
2641 if len( cancel_urls) == 0:
2642 log('Nothing selected.', sender = self)
2643 return
2645 if len( cancel_urls) == 1:
2646 title = _('Cancel download?')
2647 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2648 else:
2649 title = _('Cancel downloads?')
2650 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2652 if self.show_confirmation( message, title):
2653 services.download_status_manager.start_batch_mode()
2654 for url in cancel_urls:
2655 services.download_status_manager.cancel_by_url( url)
2656 services.download_status_manager.end_batch_mode()
2657 self.play_or_download()
2659 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2660 self.on_treeDownloads_row_activated( widget, None)
2662 def on_btnCancelAll_clicked(self, widget, *args):
2663 self.treeDownloads.get_selection().select_all()
2664 self.on_treeDownloads_row_activated( self.toolCancel, None)
2665 self.treeDownloads.get_selection().unselect_all()
2667 def on_btnDownloadedDelete_clicked(self, widget, *args):
2668 if self.active_channel is None:
2669 return
2671 channel_url = self.active_channel.url
2672 selection = self.treeAvailable.get_selection()
2673 ( model, paths ) = selection.get_selected_rows()
2675 if selection.count_selected_rows() == 0:
2676 log( 'Nothing selected - will not remove any downloaded episode.')
2677 return
2679 if selection.count_selected_rows() == 1:
2680 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2682 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2683 if episode['is_locked']:
2684 title = _('%s is locked') % episode_title
2685 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2686 self.notification(message, title)
2687 return
2689 title = _('Remove %s?') % episode_title
2690 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.")
2691 else:
2692 title = _('Remove %d episodes?') % selection.count_selected_rows()
2693 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.')
2695 locked_count = 0
2696 for path in paths:
2697 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2698 if episode['is_locked']:
2699 locked_count += 1
2701 if selection.count_selected_rows() == locked_count:
2702 title = _('Episodes are locked')
2703 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2704 self.notification(message, title)
2705 return
2706 elif locked_count > 0:
2707 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2708 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.')
2710 # if user confirms deletion, let's remove some stuff ;)
2711 if self.show_confirmation( message, title):
2712 try:
2713 # iterate over the selection, see also on_treeDownloads_row_activated
2714 for path in paths:
2715 url = model.get_value( model.get_iter( path), 0)
2716 self.active_channel.delete_episode_by_url( url)
2718 # now, clear local db cache so we can re-read it
2719 self.updateComboBox()
2720 except:
2721 log( 'Error while deleting (some) downloads.')
2723 # only delete partial files if we do not have any downloads in progress
2724 delete_partial = not services.download_status_manager.has_items()
2725 gl.clean_up_downloads(delete_partial)
2726 self.update_selected_episode_list_icons()
2727 self.play_or_download()
2729 def on_key_press(self, widget, event):
2730 # Allow tab switching with Ctrl + PgUp/PgDown
2731 if event.state & gtk.gdk.CONTROL_MASK:
2732 if event.keyval == gtk.keysyms.Page_Up:
2733 self.wNotebook.prev_page()
2734 return True
2735 elif event.keyval == gtk.keysyms.Page_Down:
2736 self.wNotebook.next_page()
2737 return True
2739 # After this code we only handle Maemo hardware keys,
2740 # so if we are not a Maemo app, we don't do anything
2741 if gpodder.interface != gpodder.MAEMO:
2742 return False
2744 if event.keyval == gtk.keysyms.F6:
2745 if self.fullscreen:
2746 self.window.unfullscreen()
2747 else:
2748 self.window.fullscreen()
2749 if event.keyval == gtk.keysyms.Escape:
2750 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2751 self.vboxChannelNavigator.set_property('visible', new_visibility)
2752 self.column_size.set_visible(not new_visibility)
2753 self.column_released.set_visible(not new_visibility)
2755 diff = 0
2756 if event.keyval == gtk.keysyms.F7: #plus
2757 diff = 1
2758 elif event.keyval == gtk.keysyms.F8: #minus
2759 diff = -1
2761 if diff != 0:
2762 selection = self.treeChannels.get_selection()
2763 (model, iter) = selection.get_selected()
2764 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2765 self.on_treeChannels_cursor_changed(self.treeChannels)
2767 def window_state_event(self, widget, event):
2768 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2769 self.fullscreen = True
2770 else:
2771 self.fullscreen = False
2773 old_minimized = self.minimized
2775 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2776 if gpodder.interface == gpodder.MAEMO:
2777 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2779 if old_minimized != self.minimized and self.tray_icon:
2780 self.gPodder.set_skip_taskbar_hint(self.minimized)
2781 elif not self.tray_icon:
2782 self.gPodder.set_skip_taskbar_hint(False)
2784 if gl.config.minimize_to_tray and self.tray_icon:
2785 self.tray_icon.set_visible(self.minimized)
2787 def uniconify_main_window(self):
2788 if self.minimized:
2789 self.gPodder.present()
2791 def iconify_main_window(self):
2792 if not self.minimized:
2793 self.gPodder.iconify()
2795 def update_podcasts_tab(self):
2796 if len(self.channels):
2797 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2798 else:
2799 self.label2.set_text(_('Podcasts'))
2801 class gPodderChannel(GladeWidget):
2802 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
2804 def new(self):
2805 global WEB_BROWSER_ICON
2806 self.changed = False
2807 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2808 self.gPodderChannel.set_title( self.channel.title)
2809 self.entryTitle.set_text( self.channel.title)
2810 self.entryURL.set_text( self.channel.url)
2812 self.LabelDownloadTo.set_text( self.channel.save_dir)
2813 self.LabelWebsite.set_text( self.channel.link)
2815 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2816 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2817 if self.channel.username:
2818 self.FeedUsername.set_text( self.channel.username)
2819 if self.channel.password:
2820 self.FeedPassword.set_text( self.channel.password)
2822 services.cover_downloader.register('cover-available', self.cover_download_finished)
2823 services.cover_downloader.request_cover(self.channel)
2825 # Hide the website button if we don't have a valid URL
2826 if not self.channel.link:
2827 self.btn_website.hide_all()
2829 b = gtk.TextBuffer()
2830 b.set_text( self.channel.description)
2831 self.channel_description.set_buffer( b)
2833 #Add Drag and Drop Support
2834 flags = gtk.DEST_DEFAULT_ALL
2835 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2836 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2837 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2838 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2840 def on_btn_website_clicked(self, widget):
2841 util.open_website(self.channel.link)
2843 def on_btnDownloadCover_clicked(self, widget):
2844 if gpodder.interface == gpodder.GUI:
2845 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2846 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2847 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2848 elif gpodder.interface == gpodder.MAEMO:
2849 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2851 if dlg.run() == gtk.RESPONSE_OK:
2852 url = dlg.get_uri()
2853 services.cover_downloader.replace_cover(self.channel, url)
2855 dlg.destroy()
2857 def on_btnClearCover_clicked(self, widget):
2858 services.cover_downloader.replace_cover(self.channel)
2860 def cover_download_finished(self, channel_url, pixbuf):
2861 if pixbuf is not None:
2862 self.imgCover.set_from_pixbuf(pixbuf)
2863 self.gPodderChannel.show()
2865 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2866 files = sel.data.strip().split('\n')
2867 if len(files) != 1:
2868 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2869 return
2871 file = files[0]
2873 if file.startswith('file://') or file.startswith('http://'):
2874 services.cover_downloader.replace_cover(self.channel, file)
2875 return
2877 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2879 def on_gPodderChannel_destroy(self, widget, *args):
2880 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2882 def on_btnOK_clicked(self, widget, *args):
2883 entered_url = self.entryURL.get_text()
2884 channel_url = self.channel.url
2886 if entered_url != channel_url:
2887 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2888 if hasattr(self, 'callback_change_url'):
2889 self.gPodderChannel.hide_all()
2890 self.callback_change_url(channel_url, entered_url)
2892 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2893 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2894 self.channel.set_custom_title( self.entryTitle.get_text())
2895 self.channel.username = self.FeedUsername.get_text().strip()
2896 self.channel.password = self.FeedPassword.get_text()
2897 self.channel.save()
2899 self.gPodderChannel.destroy()
2900 self.callback_closed()
2902 class gPodderAddPodcastDialog(GladeWidget):
2903 finger_friendly_widgets = ['btn_close', 'btn_add']
2905 def new(self):
2906 if not hasattr(self, 'url_callback'):
2907 log('No url callback set', sender=self)
2908 self.url_callback = None
2909 if hasattr(self, 'custom_label'):
2910 self.label_add.set_text(self.custom_label)
2911 if hasattr(self, 'custom_title'):
2912 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2913 if gpodder.interface == gpodder.MAEMO:
2914 self.entry_url.set_text('http://')
2916 def on_btn_close_clicked(self, widget):
2917 self.gPodderAddPodcastDialog.destroy()
2919 def on_entry_url_changed(self, widget):
2920 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2922 def on_btn_add_clicked(self, widget):
2923 url = self.entry_url.get_text()
2924 self.on_btn_close_clicked(widget)
2925 if self.url_callback is not None:
2926 self.url_callback(url)
2929 class gPodderMaemoPreferences(GladeWidget):
2930 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2932 def new(self):
2933 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2934 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2935 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2936 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2938 self.restart_required = False
2939 self.show_tray_icon.connect('clicked', self.on_restart_required)
2940 self.show_notifications.connect('clicked', self.on_restart_required)
2942 def on_restart_required(self, widget):
2943 self.restart_required = True
2945 def on_btn_advanced_clicked(self, widget):
2946 self.gPodderMaemoPreferences.destroy()
2947 gPodderConfigEditor()
2949 def on_btn_close_clicked(self, widget):
2950 self.gPodderMaemoPreferences.destroy()
2951 if self.restart_required:
2952 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2955 class gPodderProperties(GladeWidget):
2956 def new(self):
2957 if not hasattr( self, 'callback_finished'):
2958 self.callback_finished = None
2960 if gpodder.interface == gpodder.MAEMO:
2961 self.table5.hide_all() # player
2962 self.gPodderProperties.fullscreen()
2964 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2965 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2966 gl.config.connect_gtk_editable( 'player', self.openApp)
2967 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2968 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2969 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2970 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2971 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2972 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2973 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2974 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2975 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2976 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2977 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2978 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2979 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2980 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2981 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2982 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2983 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2984 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2985 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2986 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2988 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2989 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2991 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2993 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2994 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2995 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2997 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2998 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
3000 if tagging_supported():
3001 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
3002 else:
3003 self.updatetags.set_sensitive( False)
3004 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
3005 self.updatetags.set_label( new_label)
3007 # device type
3008 self.comboboxDeviceType.set_active( 0)
3009 if gl.config.device_type == 'ipod':
3010 self.comboboxDeviceType.set_active( 1)
3011 elif gl.config.device_type == 'filesystem':
3012 self.comboboxDeviceType.set_active( 2)
3013 elif gl.config.device_type == 'mtp':
3014 self.comboboxDeviceType.set_active( 3)
3016 # setup cell renderers
3017 cellrenderer = gtk.CellRendererPixbuf()
3018 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
3019 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3020 cellrenderer = gtk.CellRendererText()
3021 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
3022 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3024 cellrenderer = gtk.CellRendererPixbuf()
3025 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
3026 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3027 cellrenderer = gtk.CellRendererText()
3028 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
3029 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3031 if not hasattr(self, 'user_apps_reader'):
3032 self.user_apps_reader = UserAppsReader(['audio', 'video'])
3034 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
3035 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
3037 if gpodder.interface == gpodder.GUI:
3038 self.user_apps_reader.read()
3040 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
3041 index = self.find_active_audio_app()
3042 self.comboAudioPlayerApp.set_active(index)
3043 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
3044 index = self.find_active_video_app()
3045 self.comboVideoPlayerApp.set_active(index)
3047 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
3049 def is_row_separator(self, model, iter):
3050 return model.get_value(iter, 0) == ''
3052 def update_mountpoint( self, ipod):
3053 if ipod is None or ipod.mount_point is None:
3054 self.iPodMountpoint.set_label( '')
3055 else:
3056 self.iPodMountpoint.set_label( ipod.mount_point)
3058 def find_active_audio_app(self):
3059 index_custom = -1
3060 model = self.comboAudioPlayerApp.get_model()
3061 iter = model.get_iter_first()
3062 index = 0
3063 while iter is not None:
3064 command = model.get_value(iter, 1)
3065 if command == self.openApp.get_text():
3066 return index
3067 if index_custom < 0 and command == '':
3068 index_custom = index
3069 iter = model.iter_next(iter)
3070 index += 1
3071 # return index of custom command or first item
3072 return max(0, index_custom)
3074 def find_active_video_app( self):
3075 index_custom = -1
3076 model = self.comboVideoPlayerApp.get_model()
3077 iter = model.get_iter_first()
3078 index = 0
3079 while iter is not None:
3080 command = model.get_value(iter, 1)
3081 if command == self.openVideoApp.get_text():
3082 return index
3083 if index_custom < 0 and command == '':
3084 index_custom = index
3085 iter = model.iter_next(iter)
3086 index += 1
3087 # return index of custom command or first item
3088 return max(0, index_custom)
3090 def set_download_dir( self, new_download_dir, event = None):
3091 gl.downloaddir = self.chooserDownloadTo.get_filename()
3092 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3093 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'))
3095 if event:
3096 event.set()
3098 def on_auto_update_feeds_toggled( self, widget, *args):
3099 self.auto_update_frequency.set_sensitive(widget.get_active())
3101 def on_display_tray_icon_toggled( self, widget, *args):
3102 self.enable_notifications.set_sensitive(widget.get_active())
3103 self.minimize_to_tray.set_sensitive(widget.get_active())
3105 def on_cbCustomSyncName_toggled( self, widget, *args):
3106 self.entryCustomSyncName.set_sensitive( widget.get_active())
3108 def on_only_sync_not_played_toggled( self, widget, *args):
3109 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3110 if not widget.get_active():
3111 self.delete_episodes_marked_played.set_active(False)
3113 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3114 if widget.get_active() and self.only_sync_not_played.get_active():
3115 self.on_sync_leave.set_active(True)
3116 self.on_sync_delete.set_sensitive(not widget.get_active())
3117 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3119 def on_btnCustomSyncNameHelp_clicked( self, widget):
3120 examples = [
3121 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3122 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3123 '<i>{episode.published}</i> -&gt; <b>20070908</b> (for 08.09.2007)',
3124 '<i>{episode.pubtime}</i> -&gt; <b>1344</b> (for 13:44)',
3125 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3128 info = [
3129 _('You can specify a custom format string for the file names on your MP3 player here.'),
3130 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3131 '\n'.join( [ ' %s' % s for s in examples ])
3134 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3136 def on_gPodderProperties_destroy(self, widget, *args):
3137 self.on_btnOK_clicked( widget, *args)
3139 def on_btnConfigEditor_clicked(self, widget, *args):
3140 self.on_btnOK_clicked(widget, *args)
3141 gPodderConfigEditor()
3143 def on_comboAudioPlayerApp_changed(self, widget, *args):
3144 # find out which one
3145 iter = self.comboAudioPlayerApp.get_active_iter()
3146 model = self.comboAudioPlayerApp.get_model()
3147 command = model.get_value( iter, 1)
3148 if command == '':
3149 if self.openApp.get_text() == 'default':
3150 self.openApp.set_text('')
3151 self.openApp.set_sensitive( True)
3152 self.openApp.show()
3153 self.labelCustomCommand.show()
3154 else:
3155 self.openApp.set_text( command)
3156 self.openApp.set_sensitive( False)
3157 self.openApp.hide()
3158 self.labelCustomCommand.hide()
3160 def on_comboVideoPlayerApp_changed(self, widget, *args):
3161 # find out which one
3162 iter = self.comboVideoPlayerApp.get_active_iter()
3163 model = self.comboVideoPlayerApp.get_model()
3164 command = model.get_value(iter, 1)
3165 if command == '':
3166 if self.openVideoApp.get_text() == 'default':
3167 self.openVideoApp.set_text('')
3168 self.openVideoApp.set_sensitive(True)
3169 self.openVideoApp.show()
3170 self.labelCustomVideoCommand.show()
3171 else:
3172 self.openVideoApp.set_text(command)
3173 self.openVideoApp.set_sensitive(False)
3174 self.openVideoApp.hide()
3175 self.labelCustomVideoCommand.hide()
3177 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3178 sens = not self.cbEnvironmentVariables.get_active()
3179 self.httpProxy.set_sensitive( sens)
3180 self.ftpProxy.set_sensitive( sens)
3182 def on_comboboxDeviceType_changed(self, widget, *args):
3183 active_item = self.comboboxDeviceType.get_active()
3185 # None
3186 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3187 self.imageSyncOptions, self. separatorSyncOptions,
3188 self.on_sync_mark_played, self.on_sync_delete,
3189 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
3190 for widget in sync_widgets:
3191 if active_item == 0:
3192 widget.hide_all()
3193 else:
3194 widget.show_all()
3196 # iPod
3197 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3198 self.ipod_write_gtkpod_extended)
3199 for widget in ipod_widgets:
3200 if active_item == 1:
3201 widget.show_all()
3202 else:
3203 widget.hide_all()
3205 # filesystem-based MP3 player
3206 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3207 self.cbChannelSubfolder, self.cbCustomSyncName,
3208 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
3209 for widget in fs_widgets:
3210 if active_item == 2:
3211 widget.show_all()
3212 else:
3213 widget.hide_all()
3215 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3216 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3217 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3218 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3219 fs.set_current_folder(self.iPodMountpoint.get_label())
3220 if fs.run() == gtk.RESPONSE_OK:
3221 self.iPodMountpoint.set_label( fs.get_filename())
3222 fs.destroy()
3224 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3225 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3226 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3227 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3228 fs.set_current_folder(self.filesystemMountpoint.get_label())
3229 if fs.run() == gtk.RESPONSE_OK:
3230 self.filesystemMountpoint.set_label( fs.get_filename())
3231 fs.destroy()
3233 def on_btnOK_clicked(self, widget, *args):
3234 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3235 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3237 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3238 new_download_dir = self.chooserDownloadTo.get_filename()
3239 download_dir_size = util.calculate_size( gl.downloaddir)
3240 download_dir_size_string = gl.format_filesize( download_dir_size)
3241 event = Event()
3243 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3244 dlg.vbox.set_spacing( 5)
3245 dlg.set_border_width( 5)
3247 label = gtk.Label()
3248 label.set_line_wrap( True)
3249 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3250 myprogressbar = gtk.ProgressBar()
3252 # put it all together
3253 dlg.vbox.pack_start( label)
3254 dlg.vbox.pack_end( myprogressbar)
3256 # switch windows
3257 dlg.show_all()
3258 self.gPodderProperties.hide_all()
3260 # hide action area and separator line
3261 dlg.action_area.hide()
3262 dlg.set_has_separator( False)
3264 args = ( new_download_dir, event, )
3266 thread = Thread( target = self.set_download_dir, args = args)
3267 thread.start()
3269 while not event.isSet():
3270 try:
3271 new_download_dir_size = util.calculate_size( new_download_dir)
3272 except:
3273 new_download_dir_size = 0
3274 if download_dir_size > 0:
3275 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3276 else:
3277 fract = 0.0
3278 if fract < 0.99:
3279 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3280 else:
3281 myprogressbar.set_text( _('Finishing... please wait.'))
3282 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3283 event.wait( 0.1)
3284 while gtk.events_pending():
3285 gtk.main_iteration( False)
3287 dlg.destroy()
3289 device_type = self.comboboxDeviceType.get_active()
3290 if device_type == 0:
3291 gl.config.device_type = 'none'
3292 elif device_type == 1:
3293 gl.config.device_type = 'ipod'
3294 elif device_type == 2:
3295 gl.config.device_type = 'filesystem'
3296 elif device_type == 3:
3297 gl.config.device_type = 'mtp'
3298 self.gPodderProperties.destroy()
3299 if self.callback_finished:
3300 self.callback_finished()
3303 class gPodderEpisode(GladeWidget):
3304 finger_friendly_widgets = ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3306 def new(self):
3307 setattr(self, 'episode', None)
3308 setattr(self, 'download_callback', None)
3309 setattr(self, 'play_callback', None)
3310 self.gPodderEpisode.connect('delete-event', self.on_delete_event)
3311 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3312 services.download_status_manager.register('list-changed', self.on_download_status_changed)
3313 services.download_status_manager.register('progress-detail', self.on_download_status_progress)
3314 self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
3315 if gl.config.enable_html_shownotes:
3316 try:
3317 import gtkhtml2
3318 setattr(self, 'have_gtkhtml2', True)
3319 # Generate a HTML view and remove the textview
3320 setattr(self, 'htmlview', gtkhtml2.View())
3321 self.scrolled_window.remove(self.scrolled_window.get_child())
3322 self.scrolled_window.add(self.htmlview)
3323 self.textview = None
3324 self.htmlview.set_document(gtkhtml2.Document())
3325 self.htmlview.show()
3326 except ImportError:
3327 log('Install gtkhtml2 if you want HTML shownotes', sender=self)
3328 setattr(self, 'have_gtkhtml2', False)
3329 else:
3330 setattr(self, 'have_gtkhtml2', False)
3332 def show(self, episode, download_callback, play_callback):
3333 self.episode = episode
3334 self.download_callback = download_callback
3335 self.play_callback = play_callback
3337 self.gPodderEpisode.set_title(self.episode.title)
3339 if self.have_gtkhtml2:
3340 import gtkhtml2
3341 d = gtkhtml2.Document()
3342 d.open_stream('text/html')
3343 d.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3344 d.close_stream()
3345 self.htmlview.set_document(d)
3346 else:
3347 b = gtk.TextBuffer()
3348 self.textview.set_buffer(b)
3350 self.hide_show_widgets()
3351 self.gPodderEpisode.show()
3353 # Make sure the window comes up right now:
3354 while gtk.events_pending():
3355 gtk.main_iteration(False)
3357 # Now do the stuff that takes a bit longer...
3358 heading = self.episode.title
3359 subheading = 'from %s' % (self.episode.channel.title)
3360 description = self.episode.description
3361 footer = []
3363 if self.have_gtkhtml2:
3364 import gtkhtml2
3365 d.connect('link-clicked', lambda d, url: util.open_website(url))
3366 def request_url(document, url, stream):
3367 def opendata(url, stream):
3368 fp = urllib2.urlopen(url)
3369 data = fp.read(1024*10)
3370 while data != '':
3371 stream.write(data)
3372 data = fp.read(1024*10)
3373 stream.close()
3374 Thread(target=opendata, args=[url, stream]).start()
3375 d.connect('request-url', request_url)
3376 d.clear()
3377 d.open_stream('text/html')
3378 d.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3379 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)))
3380 d.write_stream(self.episode.description)
3381 if len(footer):
3382 d.write_stream('<hr style="border: 1px #eeeeee solid;">')
3383 d.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils.escape(f) for f in footer))),))
3384 d.write_stream('</p></body></html>')
3385 d.close_stream()
3386 else:
3387 b.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
3388 b.create_tag('subheading', scale=pango.SCALE_SMALL)
3389 b.create_tag('footer', scale=pango.SCALE_SMALL)
3391 b.insert_with_tags_by_name(b.get_end_iter(), heading, 'heading')
3392 b.insert_at_cursor('\n')
3393 b.insert_with_tags_by_name(b.get_end_iter(), subheading, 'subheading')
3394 b.insert_at_cursor('\n\n')
3395 b.insert(b.get_end_iter(), util.remove_html_tags(description))
3396 if len(footer):
3397 b.insert_at_cursor('\n\n')
3398 b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
3400 services.download_status_manager.request_progress_detail(self.episode.url)
3402 def on_cancel(self, widget):
3403 services.download_status_manager.cancel_by_url(self.episode.url)
3405 def on_delete_event(self, widget, event):
3406 # Avoid destroying the dialog, simply hide
3407 self.on_close(widget)
3408 return True
3410 def on_close(self, widget):
3411 self.episode = None
3412 if self.have_gtkhtml2:
3413 import gtkhtml2
3414 self.htmlview.set_document(gtkhtml2.Document())
3415 else:
3416 self.textview.get_buffer().set_text('')
3417 self.gPodderEpisode.hide()
3419 def on_download_status_changed(self, episode_urls, channel_urls):
3420 if self.gPodderEpisode.get_property('visible'):
3421 self.hide_show_widgets()
3422 else:
3423 log('download status changed, but not visible', sender=self)
3425 def on_download_status_progress(self, url, progress, speed):
3426 if self.episode is None:
3427 return
3429 if url == self.episode.url:
3430 progress = float(min(100.0,max(0.0,progress)))
3431 self.download_progress.set_fraction(progress/100.0)
3432 self.download_progress.set_text('Downloading: %d%% (%s)' % (progress, speed))
3434 def hide_show_widgets(self):
3435 is_downloading = services.download_status_manager.is_download_in_progress(self.episode.url)
3436 if is_downloading:
3437 self.download_progress.show_all()
3438 self.btnCancel.show_all()
3439 self.btnPlay.hide_all()
3440 self.btnDownload.hide_all()
3441 else:
3442 self.download_progress.hide_all()
3443 self.btnCancel.hide_all()
3444 if os.path.exists(self.episode.local_filename()):
3445 if self.episode.file_type() in ('audio', 'video'):
3446 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3447 else:
3448 self.btnPlay.set_label(gtk.STOCK_OPEN)
3449 self.btnPlay.set_use_stock(True)
3450 self.btnPlay.show_all()
3451 self.btnDownload.hide_all()
3452 else:
3453 self.btnPlay.hide_all()
3454 self.btnDownload.show_all()
3456 def on_download(self, widget):
3457 if self.download_callback:
3458 self.download_callback()
3460 def on_playback(self, widget):
3461 if self.play_callback:
3462 self.play_callback()
3463 self.on_close(widget)
3465 class gPodderSync(GladeWidget):
3466 def new(self):
3467 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3469 self.device.register('progress', self.on_progress)
3470 self.device.register('sub-progress', self.on_sub_progress)
3471 self.device.register('status', self.on_status)
3472 self.device.register('done', self.on_done)
3474 def on_progress(self, pos, max, text=None):
3475 if text is None:
3476 text = _('%d of %d done') % (pos, max)
3477 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3478 util.idle_add(self.progressbar.set_text, text)
3480 def on_sub_progress(self, percentage):
3481 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3483 def on_status(self, status):
3484 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3486 def on_done(self):
3487 util.idle_add(self.gPodderSync.destroy)
3488 if not self.gPodder.minimized:
3489 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3491 def on_gPodderSync_destroy(self, widget, *args):
3492 self.device.unregister('progress', self.on_progress)
3493 self.device.unregister('sub-progress', self.on_sub_progress)
3494 self.device.unregister('status', self.on_status)
3495 self.device.unregister('done', self.on_done)
3496 self.device.cancel()
3498 def on_cancel_button_clicked(self, widget, *args):
3499 self.device.cancel()
3502 class gPodderOpmlLister(GladeWidget):
3503 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3505 def new(self):
3506 # initiate channels list
3507 self.channels = []
3508 self.callback_for_channel = None
3509 self.callback_finished = None
3511 if hasattr(self, 'custom_title'):
3512 self.gPodderOpmlLister.set_title(self.custom_title)
3513 if hasattr(self, 'hide_url_entry'):
3514 self.hboxOpmlUrlEntry.hide_all()
3515 new_parent = self.notebookChannelAdder.get_parent()
3516 new_parent.remove(self.notebookChannelAdder)
3517 self.vboxOpmlImport.reparent(new_parent)
3519 self.setup_treeview(self.treeviewChannelChooser)
3520 self.setup_treeview(self.treeviewTopPodcastsChooser)
3521 self.setup_treeview(self.treeviewYouTubeChooser)
3523 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
3525 def setup_treeview(self, tv):
3526 togglecell = gtk.CellRendererToggle()
3527 togglecell.set_property( 'activatable', True)
3528 togglecell.connect( 'toggled', self.callback_edited)
3529 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3531 titlecell = gtk.CellRendererText()
3532 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3533 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3535 for itemcolumn in ( togglecolumn, titlecolumn ):
3536 tv.append_column(itemcolumn)
3538 def callback_edited( self, cell, path):
3539 model = self.get_treeview().get_model()
3541 url = model[path][2]
3543 model[path][0] = not model[path][0]
3544 if model[path][0]:
3545 self.channels.append( url)
3546 else:
3547 self.channels.remove( url)
3549 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
3551 def get_selected_channels(self, tab=None):
3552 channels = []
3554 model = self.get_treeview(tab).get_model()
3555 if model is not None:
3556 for row in model:
3557 if row[0]:
3558 channels.append(row[2])
3560 return channels
3562 def on_change_tab(self, tab):
3563 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
3565 def thread_finished(self, model, tab=0):
3566 if tab == 1:
3567 tv = self.treeviewTopPodcastsChooser
3568 elif tab == 2:
3569 tv = self.treeviewYouTubeChooser
3570 self.entryYoutubeSearch.set_sensitive(True)
3571 self.btnSearchYouTube.set_sensitive(True)
3572 self.btnOK.set_sensitive(False)
3573 else:
3574 tv = self.treeviewChannelChooser
3575 self.btnDownloadOpml.set_sensitive(True)
3576 self.entryURL.set_sensitive(True)
3577 self.channels = []
3579 tv.set_model(model)
3580 tv.set_sensitive(True)
3582 def thread_func(self, tab=0):
3583 if tab == 1:
3584 model = opml.Importer(gl.config.toplist_url).get_model()
3585 if len(model) == 0:
3586 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3587 elif tab == 2:
3588 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
3589 if len(model) == 0:
3590 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
3591 else:
3592 model = opml.Importer(self.entryURL.get_text()).get_model()
3593 if len(model) == 0:
3594 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3596 util.idle_add(self.thread_finished, model, tab)
3598 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3599 if callback_for_channel:
3600 self.callback_for_channel = callback_for_channel
3601 if callback_finished:
3602 self.callback_finished = callback_finished
3603 self.entryURL.set_text( url)
3604 self.btnDownloadOpml.set_sensitive( False)
3605 self.entryURL.set_sensitive( False)
3606 self.btnOK.set_sensitive( False)
3607 self.treeviewChannelChooser.set_sensitive( False)
3608 Thread( target = self.thread_func).start()
3609 Thread( target = lambda: self.thread_func(1)).start()
3611 def select_all( self, value ):
3612 enabled = False
3613 model = self.get_treeview().get_model()
3614 if model is not None:
3615 for row in model:
3616 row[0] = value
3617 if value:
3618 enabled = True
3619 self.btnOK.set_sensitive(enabled)
3621 def on_gPodderOpmlLister_destroy(self, widget, *args):
3622 pass
3624 def on_btnDownloadOpml_clicked(self, widget, *args):
3625 self.get_channels_from_url( self.entryURL.get_text())
3627 def on_btnSearchYouTube_clicked(self, widget, *args):
3628 self.entryYoutubeSearch.set_sensitive(False)
3629 self.treeviewYouTubeChooser.set_sensitive(False)
3630 self.btnSearchYouTube.set_sensitive(False)
3631 Thread(target = lambda: self.thread_func(2)).start()
3633 def on_btnSelectAll_clicked(self, widget, *args):
3634 self.select_all(True)
3636 def on_btnSelectNone_clicked(self, widget, *args):
3637 self.select_all(False)
3639 def on_btnOK_clicked(self, widget, *args):
3640 self.channels = self.get_selected_channels()
3641 self.gPodderOpmlLister.destroy()
3643 # add channels that have been selected
3644 for url in self.channels:
3645 if self.callback_for_channel:
3646 self.callback_for_channel( url)
3648 if self.callback_finished:
3649 util.idle_add(self.callback_finished)
3651 def on_btnCancel_clicked(self, widget, *args):
3652 self.gPodderOpmlLister.destroy()
3654 def on_entryYoutubeSearch_key_press_event(self, widget, event):
3655 if event.keyval == gtk.keysyms.Return:
3656 self.on_btnSearchYouTube_clicked(widget)
3658 def get_treeview(self, tab=None):
3659 if tab is None:
3660 tab = self.notebookChannelAdder.get_current_page()
3662 if tab == 0:
3663 return self.treeviewChannelChooser
3664 elif tab == 1:
3665 return self.treeviewTopPodcastsChooser
3666 else:
3667 return self.treeviewYouTubeChooser
3669 class gPodderEpisodeSelector( GladeWidget):
3670 """Episode selection dialog
3672 Optional keyword arguments that modify the behaviour of this dialog:
3674 - callback: Function that takes 1 parameter which is a list of
3675 the selected episodes (or empty list when none selected)
3676 - remove_callback: Function that takes 1 parameter which is a list
3677 of episodes that should be "removed" (see below)
3678 (default is None, which means remove not possible)
3679 - remove_action: Label for the "remove" action (default is "Remove")
3680 - remove_finished: Callback after all remove callbacks have finished
3681 (default is None, also depends on remove_callback)
3682 It will get a list of episode URLs that have been
3683 removed, so the main UI can update those
3684 - episodes: List of episodes that are presented for selection
3685 - selected: (optional) List of boolean variables that define the
3686 default checked state for the given episodes
3687 - selected_default: (optional) The default boolean value for the
3688 checked state if no other value is set
3689 (default is False)
3690 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3691 columns, the name is the attribute name of the episode to be
3692 read from each episode object. The sort name is the
3693 attribute name of the episode to be used to sort this column.
3694 If the sort_name is None it will use the attribute name for
3695 sorting. The sort type is the type of the sort column.
3696 The caption attribute is the text that appear as column caption
3697 (default is [('title_and_description', None, None, 'Episode'),])
3698 - title: (optional) The title of the window + heading
3699 - instructions: (optional) A one-line text describing what the
3700 user should select / what the selection is for
3701 - stock_ok_button: (optional) Will replace the "OK" button with
3702 another GTK+ stock item to be used for the
3703 affirmative button of the dialog (e.g. can
3704 be gtk.STOCK_DELETE when the episodes to be
3705 selected will be deleted after closing the
3706 dialog)
3707 - selection_buttons: (optional) A dictionary with labels as
3708 keys and callbacks as values; for each
3709 key a button will be generated, and when
3710 the button is clicked, the callback will
3711 be called for each episode and the return
3712 value of the callback (True or False) will
3713 be the new selected state of the episode
3714 - size_attribute: (optional) The name of an attribute of the
3715 supplied episode objects that can be used to
3716 calculate the size of an episode; set this to
3717 None if no total size calculation should be
3718 done (in cases where total size is useless)
3719 (default is 'length')
3720 - tooltip_attribute: (optional) The name of an attribute of
3721 the supplied episode objects that holds
3722 the text for the tooltips when hovering
3723 over an episode (default is 'description')
3726 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3728 COLUMN_INDEX = 0
3729 COLUMN_TOOLTIP = 1
3730 COLUMN_TOGGLE = 2
3731 COLUMN_ADDITIONAL = 3
3733 def new( self):
3734 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
3735 if not hasattr( self, 'callback'):
3736 self.callback = None
3738 if not hasattr(self, 'remove_callback'):
3739 self.remove_callback = None
3741 if not hasattr(self, 'remove_action'):
3742 self.remove_action = _('Remove')
3744 if not hasattr(self, 'remove_finished'):
3745 self.remove_finished = None
3747 if not hasattr( self, 'episodes'):
3748 self.episodes = []
3750 if not hasattr( self, 'size_attribute'):
3751 self.size_attribute = 'length'
3753 if not hasattr(self, 'tooltip_attribute'):
3754 self.tooltip_attribute = 'description'
3756 if not hasattr( self, 'selection_buttons'):
3757 self.selection_buttons = {}
3759 if not hasattr( self, 'selected_default'):
3760 self.selected_default = False
3762 if not hasattr( self, 'selected'):
3763 self.selected = [self.selected_default]*len(self.episodes)
3765 if len(self.selected) < len(self.episodes):
3766 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3768 if not hasattr( self, 'columns'):
3769 self.columns = (('title_and_description', None, None, _('Episode')),)
3771 if hasattr( self, 'title'):
3772 self.gPodderEpisodeSelector.set_title( self.title)
3773 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3775 if gpodder.interface == gpodder.MAEMO:
3776 self.labelHeading.hide()
3778 if hasattr( self, 'instructions'):
3779 self.labelInstructions.set_text( self.instructions)
3780 self.labelInstructions.show_all()
3782 if hasattr(self, 'stock_ok_button'):
3783 if self.stock_ok_button == 'gpodder-download':
3784 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3785 self.btnOK.set_label(_('Download'))
3786 else:
3787 self.btnOK.set_label(self.stock_ok_button)
3788 self.btnOK.set_use_stock(True)
3790 # check/uncheck column
3791 toggle_cell = gtk.CellRendererToggle()
3792 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3793 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3795 next_column = self.COLUMN_ADDITIONAL
3796 for name, sort_name, sort_type, caption in self.columns:
3797 renderer = gtk.CellRendererText()
3798 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3799 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3800 column.set_resizable( True)
3801 # Only set "expand" on the first column (so more text is displayed there)
3802 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3803 if sort_name is not None:
3804 column.set_sort_column_id(next_column+1)
3805 else:
3806 column.set_sort_column_id(next_column)
3807 self.treeviewEpisodes.append_column( column)
3808 next_column += 1
3810 if sort_name is not None:
3811 # add the sort column
3812 column = gtk.TreeViewColumn()
3813 column.set_visible(False)
3814 self.treeviewEpisodes.append_column( column)
3815 next_column += 1
3817 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3818 # add string column type plus sort column type if it exists
3819 for name, sort_name, sort_type, caption in self.columns:
3820 column_types.append(gobject.TYPE_STRING)
3821 if sort_name is not None:
3822 column_types.append(sort_type)
3823 self.model = gtk.ListStore( *column_types)
3825 tooltip = None
3826 for index, episode in enumerate( self.episodes):
3827 if self.tooltip_attribute is not None:
3828 try:
3829 tooltip = getattr(episode, self.tooltip_attribute)
3830 except:
3831 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3832 tooltip = None
3833 row = [ index, tooltip, self.selected[index] ]
3834 for name, sort_name, sort_type, caption in self.columns:
3835 if not hasattr(episode, name):
3836 log('Warning: Missing attribute "%s"', name, sender=self)
3837 row.append(None)
3838 else:
3839 row.append(getattr( episode, name))
3841 if sort_name is not None:
3842 if not hasattr(episode, sort_name):
3843 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3844 row.append(None)
3845 else:
3846 row.append(getattr( episode, sort_name))
3847 self.model.append( row)
3849 if self.remove_callback is not None:
3850 self.btnRemoveAction.show()
3851 self.btnRemoveAction.set_label(self.remove_action)
3853 # connect to tooltip signals
3854 if self.tooltip_attribute is not None:
3855 try:
3856 self.treeviewEpisodes.set_property('has-tooltip', True)
3857 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3858 except:
3859 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3860 self.last_tooltip_episode = None
3861 self.episode_list_can_tooltip = True
3863 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3864 self.treeviewEpisodes.set_rules_hint( True)
3865 self.treeviewEpisodes.set_model( self.model)
3866 self.treeviewEpisodes.columns_autosize()
3867 self.calculate_total_size()
3869 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3870 # With get_bin_window, we get the window that contains the rows without
3871 # the header. The Y coordinate of this window will be the height of the
3872 # treeview header. This is the amount we have to subtract from the
3873 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3874 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3875 y -= x_bin
3876 y -= y_bin
3877 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3879 if not self.episode_list_can_tooltip:
3880 self.last_tooltip_episode = None
3881 return False
3883 if path is not None:
3884 model = treeview.get_model()
3885 iter = model.get_iter(path)
3886 index = model.get_value(iter, self.COLUMN_INDEX)
3887 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3888 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3889 self.last_tooltip_episode = None
3890 return False
3891 self.last_tooltip_episode = index
3893 if description is not None:
3894 tooltip.set_text(description)
3895 return True
3896 else:
3897 return False
3899 self.last_tooltip_episode = None
3900 return False
3902 def treeview_episodes_button_pressed(self, treeview, event):
3903 if event.button == 3:
3904 menu = gtk.Menu()
3906 if len(self.selection_buttons):
3907 for label in self.selection_buttons:
3908 item = gtk.MenuItem(label)
3909 item.connect('activate', self.custom_selection_button_clicked, label)
3910 menu.append(item)
3911 menu.append(gtk.SeparatorMenuItem())
3913 item = gtk.MenuItem(_('Select all'))
3914 item.connect('activate', self.on_btnCheckAll_clicked)
3915 menu.append(item)
3917 item = gtk.MenuItem(_('Select none'))
3918 item.connect('activate', self.on_btnCheckNone_clicked)
3919 menu.append(item)
3921 menu.show_all()
3922 # Disable tooltips while we are showing the menu, so
3923 # the tooltip will not appear over the menu
3924 self.episode_list_can_tooltip = False
3925 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3926 menu.popup(None, None, None, event.button, event.time)
3928 return True
3930 def episode_list_allow_tooltips(self):
3931 self.episode_list_can_tooltip = True
3933 def calculate_total_size( self):
3934 if self.size_attribute is not None:
3935 (total_size, count) = (0, 0)
3936 for episode in self.get_selected_episodes():
3937 try:
3938 total_size += int(getattr( episode, self.size_attribute))
3939 count += 1
3940 except:
3941 log( 'Cannot get size for %s', episode.title, sender = self)
3943 text = []
3944 if count == 0:
3945 text.append(_('Nothing selected'))
3946 elif count == 1:
3947 text.append(_('One episode selected'))
3948 else:
3949 text.append(_('%d episodes selected') % count)
3950 if total_size > 0:
3951 text.append(_('total size: %s') % gl.format_filesize(total_size))
3952 self.labelTotalSize.set_text(', '.join(text))
3953 self.btnOK.set_sensitive(count>0)
3954 self.btnRemoveAction.set_sensitive(count>0)
3955 if count > 0:
3956 self.btnCancel.set_label(gtk.STOCK_CANCEL)
3957 else:
3958 self.btnCancel.set_label(gtk.STOCK_CLOSE)
3959 else:
3960 self.btnOK.set_sensitive(False)
3961 self.btnRemoveAction.set_sensitive(False)
3962 for index, row in enumerate(self.model):
3963 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3964 self.btnOK.set_sensitive(True)
3965 self.btnRemoveAction.set_sensitive(True)
3966 break
3967 self.labelTotalSize.set_text('')
3969 def toggle_cell_handler( self, cell, path):
3970 model = self.treeviewEpisodes.get_model()
3971 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3973 self.calculate_total_size()
3975 def custom_selection_button_clicked(self, button, label):
3976 callback = self.selection_buttons[label]
3978 for index, row in enumerate( self.model):
3979 new_value = callback( self.episodes[index])
3980 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3982 self.calculate_total_size()
3984 def on_btnCheckAll_clicked( self, widget):
3985 for row in self.model:
3986 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3988 self.calculate_total_size()
3990 def on_btnCheckNone_clicked( self, widget):
3991 for row in self.model:
3992 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3994 self.calculate_total_size()
3996 def on_remove_action_activate(self, widget):
3997 episodes = self.get_selected_episodes(remove_episodes=True)
3999 urls = []
4000 for episode in episodes:
4001 urls.append(episode.url)
4002 self.remove_callback(episode)
4004 if self.remove_finished is not None:
4005 self.remove_finished(urls)
4006 self.calculate_total_size()
4008 def get_selected_episodes( self, remove_episodes=False):
4009 selected_episodes = []
4011 for index, row in enumerate( self.model):
4012 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
4013 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
4015 if remove_episodes:
4016 for episode in selected_episodes:
4017 index = self.episodes.index(episode)
4018 iter = self.model.get_iter_first()
4019 while iter is not None:
4020 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
4021 self.model.remove(iter)
4022 break
4023 iter = self.model.iter_next(iter)
4025 return selected_episodes
4027 def on_btnOK_clicked( self, widget):
4028 self.gPodderEpisodeSelector.destroy()
4029 if self.callback is not None:
4030 self.callback( self.get_selected_episodes())
4032 def on_btnCancel_clicked( self, widget):
4033 self.gPodderEpisodeSelector.destroy()
4034 if self.callback is not None:
4035 self.callback([])
4037 class gPodderConfigEditor(GladeWidget):
4038 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
4040 def new(self):
4041 name_column = gtk.TreeViewColumn(_('Setting'))
4042 name_renderer = gtk.CellRendererText()
4043 name_column.pack_start(name_renderer)
4044 name_column.add_attribute(name_renderer, 'text', 0)
4045 name_column.add_attribute(name_renderer, 'style', 5)
4046 self.configeditor.append_column(name_column)
4048 value_column = gtk.TreeViewColumn(_('Set to'))
4049 value_check_renderer = gtk.CellRendererToggle()
4050 value_column.pack_start(value_check_renderer, expand=False)
4051 value_column.add_attribute(value_check_renderer, 'active', 7)
4052 value_column.add_attribute(value_check_renderer, 'visible', 6)
4053 value_column.add_attribute(value_check_renderer, 'activatable', 6)
4054 value_check_renderer.connect('toggled', self.value_toggled)
4056 value_renderer = gtk.CellRendererText()
4057 value_column.pack_start(value_renderer)
4058 value_column.add_attribute(value_renderer, 'text', 2)
4059 value_column.add_attribute(value_renderer, 'visible', 4)
4060 value_column.add_attribute(value_renderer, 'editable', 4)
4061 value_column.add_attribute(value_renderer, 'style', 5)
4062 value_renderer.connect('edited', self.value_edited)
4063 self.configeditor.append_column(value_column)
4065 self.model = gl.config.model()
4066 self.filter = self.model.filter_new()
4067 self.filter.set_visible_func(self.visible_func)
4069 self.configeditor.set_model(self.filter)
4070 self.configeditor.set_rules_hint(True)
4072 def visible_func(self, model, iter, user_data=None):
4073 text = self.entryFilter.get_text().lower()
4074 if text == '':
4075 return True
4076 else:
4077 # either the variable name or its value
4078 return (text in model.get_value(iter, 0).lower() or
4079 text in model.get_value(iter, 2).lower())
4081 def value_edited(self, renderer, path, new_text):
4082 model = self.configeditor.get_model()
4083 iter = model.get_iter(path)
4084 name = model.get_value(iter, 0)
4085 type_cute = model.get_value(iter, 1)
4087 if not gl.config.update_field(name, new_text):
4088 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))
4090 def value_toggled(self, renderer, path):
4091 model = self.configeditor.get_model()
4092 iter = model.get_iter(path)
4093 field_name = model.get_value(iter, 0)
4094 field_type = model.get_value(iter, 3)
4096 # Flip the boolean config flag
4097 if field_type == bool:
4098 gl.config.toggle_flag(field_name)
4100 def on_entryFilter_changed(self, widget):
4101 self.filter.refilter()
4103 def on_btnShowAll_clicked(self, widget):
4104 self.entryFilter.set_text('')
4105 self.entryFilter.grab_focus()
4107 def on_btnClose_clicked(self, widget):
4108 self.gPodderConfigEditor.destroy()
4110 class gPodderPlaylist(GladeWidget):
4111 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4113 def new(self):
4114 self.linebreak = '\n'
4115 if gl.config.mp3_player_playlist_win_path:
4116 self.linebreak = '\r\n'
4117 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
4118 if self.mountpoint == '/':
4119 self.mountpoint = gl.config.mp3_player_folder
4120 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
4121 self.playlist_file = os.path.join(self.mountpoint,
4122 gl.config.mp3_player_playlist_file)
4123 icon_theme = gtk.icon_theme_get_default()
4124 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4126 # add column two
4127 check_cell = gtk.CellRendererToggle()
4128 check_cell.set_property('activatable', True)
4129 check_cell.connect('toggled', self.cell_toggled)
4130 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4131 self.treeviewPlaylist.append_column(check_column)
4133 # add column three
4134 column = gtk.TreeViewColumn(_('Filename'))
4135 icon_cell = gtk.CellRendererPixbuf()
4136 column.pack_start(icon_cell, False)
4137 column.add_attribute(icon_cell, 'pixbuf', 0)
4138 filename_cell = gtk.CellRendererText()
4139 column.pack_start(filename_cell, True)
4140 column.add_attribute(filename_cell, 'text', 2)
4142 column.set_resizable(True)
4143 self.treeviewPlaylist.append_column(column)
4145 # Make treeview reorderable
4146 self.treeviewPlaylist.set_reorderable(True)
4148 # init liststore
4149 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4150 self.treeviewPlaylist.set_model(self.playlist)
4152 # read device and playlist and fill the TreeView
4153 self.m3u = self.read_m3u()
4154 self.device = self.read_device()
4155 self.write2gui()
4157 def cell_toggled(self, cellrenderertoggle, path):
4158 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4159 it = liststore.get_iter(path)
4160 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4162 def on_btnCancelPlaylist_clicked(self, widget):
4163 self.gPodderPlaylist.destroy()
4165 def on_btnSavePlaylist_clicked(self, widget):
4166 self.write_m3u()
4167 self.gPodderPlaylist.destroy()
4169 def read_m3u(self):
4171 read all files from the existing playlist
4173 tracks = []
4174 log("Read data from the playlistfile %s" % self.playlist_file)
4175 if os.path.exists(self.playlist_file):
4176 for line in open(self.playlist_file, 'r'):
4177 if not line.startswith('#EXT'):
4178 if line.startswith('#'):
4179 tracks.append([False, line[1:].strip()])
4180 else:
4181 tracks.append([True, line.strip()])
4182 return tracks
4184 def build_extinf(self, filename):
4185 if gl.config.mp3_player_playlist_win_path:
4186 filename = filename.replace('\\', os.sep)
4188 # rebuild the whole filename including the mountpoint
4189 if gl.config.mp3_player_playlist_absolute_path:
4190 absfile = self.mountpoint + filename
4191 else:
4192 absfile = util.rel2abs(filename, os.path.dirname(self.playlist_file))
4194 # read the title from the mp3/ogg tag
4195 metadata = libtagupdate.get_tags_from_file(absfile)
4196 if 'title' in metadata and metadata['title']:
4197 title = metadata['title']
4198 else:
4199 # fallback: use the basename of the file
4200 (title, extension) = os.path.splitext(os.path.basename(filename))
4202 return "#EXTINF:0,%s%s" % (title.strip(), self.linebreak)
4204 def write_m3u(self):
4206 write the list into the playlist on the device
4208 log('Writing playlist file: %s', self.playlist_file, sender=self)
4209 playlist_folder = os.path.split(self.playlist_file)[0]
4210 if not util.make_directory(playlist_folder):
4211 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4212 else:
4213 try:
4214 fp = open(self.playlist_file, 'w')
4215 fp.write('#EXTM3U%s' % self.linebreak)
4216 for icon, checked, filename in self.playlist:
4217 fp.write(self.build_extinf(filename))
4218 if not checked:
4219 fp.write('#')
4220 fp.write(filename)
4221 fp.write(self.linebreak)
4222 fp.close()
4223 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4224 except IOError, ioe:
4225 self.show_message(str(ioe), _('Error writing playlist file'))
4227 def read_device(self):
4229 read all files from the device
4231 log('Reading files from %s', gl.config.mp3_player_folder, sender=self)
4232 tracks = []
4233 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4234 for file in files:
4235 filename = os.path.join(root, file)
4237 if filename == self.playlist_file:
4238 # We don't want to have our playlist file as
4239 # an entry in our file list, so skip it!
4240 break
4242 if gl.config.mp3_player_playlist_absolute_path:
4243 filename = filename[len(self.mountpoint):]
4244 else:
4245 filename = util.relpath(os.path.dirname(self.playlist_file),
4246 os.path.dirname(filename)) + \
4247 os.sep + os.path.basename(filename)
4249 if gl.config.mp3_player_playlist_win_path:
4250 filename = filename.replace(os.sep, '\\')
4252 tracks.append(filename)
4253 return tracks
4255 def write2gui(self):
4256 # add the files from the device to the list only when
4257 # they are not yet in the playlist
4258 # mark this files as NEW
4259 for filename in self.device[:]:
4260 m3ulist = [file[1] for file in self.m3u]
4261 if filename not in m3ulist:
4262 self.playlist.append([self.icon_new, False, filename])
4264 # add the files from the playlist to the list only when
4265 # they are on the device
4266 for checked, filename in self.m3u[:]:
4267 if filename in self.device:
4268 self.playlist.append([None, checked, filename])
4270 class gPodderDependencyManager(GladeWidget):
4271 def new(self):
4272 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4273 self.treeview_components.append_column(col_name)
4274 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4275 self.treeview_components.append_column(col_installed)
4276 self.treeview_components.set_model(services.dependency_manager.get_model())
4277 self.btn_about.set_sensitive(False)
4279 def on_btn_about_clicked(self, widget):
4280 selection = self.treeview_components.get_selection()
4281 model, iter = selection.get_selected()
4282 if iter is not None:
4283 title = model.get_value(iter, 0)
4284 description = model.get_value(iter, 1)
4285 available = model.get_value(iter, 3)
4286 missing = model.get_value(iter, 4)
4288 if not available:
4289 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4291 self.show_message(description, title)
4293 def on_btn_install_clicked(self, widget):
4294 # TODO: Implement package manager integration
4295 pass
4297 def on_treeview_components_cursor_changed(self, treeview):
4298 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4299 # TODO: If installing is possible, enable btn_install
4301 def on_gPodderDependencyManager_response(self, dialog, response_id):
4302 self.gPodderDependencyManager.destroy()
4304 class gPodderWelcome(GladeWidget):
4305 finger_friendly_widgets = ['btnOPML', 'btnMygPodder', 'btnCancel']
4307 def new(self):
4308 pass
4310 def on_show_example_podcasts(self, button):
4311 self.gPodderWelcome.destroy()
4312 self.show_example_podcasts_callback(None)
4314 def on_setup_my_gpodder(self, gpodder):
4315 self.gPodderWelcome.destroy()
4316 self.setup_my_gpodder_callback(None)
4318 def on_btnCancel_clicked(self, button):
4319 self.gPodderWelcome.destroy()
4321 def main():
4322 gobject.threads_init()
4323 gtk.window_set_default_icon_name( 'gpodder')
4325 gPodder().run()