Update the UI more efficiently, make it much faster
[gpodder.git] / src / gpodder / gui.py
blob02967f5da78c4e66ccceb188a739474ad8ef6a94
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import util
43 from gpodder import opml
44 from gpodder import services
45 from gpodder import sync
46 from gpodder import download
47 from gpodder import SimpleGladeApp
48 from gpodder import my
49 from gpodder.liblogger import log
50 from gpodder.dbsqlite import db
51 from gpodder import resolver
53 try:
54 from gpodder import trayicon
55 have_trayicon = True
56 except Exception, exc:
57 log('Warning: Could not import gpodder.trayicon.', traceback=True)
58 log('Warning: This probably means your PyGTK installation is too old!')
59 have_trayicon = False
61 from libpodcasts import podcastChannel
62 from libpodcasts import LocalDBReader
63 from libpodcasts import podcastItem
64 from libpodcasts import channels_to_model
65 from libpodcasts import update_channel_model_by_iter
66 from libpodcasts import load_channels
67 from libpodcasts import update_channels
68 from libpodcasts import save_channels
69 from libpodcasts import can_restore_from_opml
70 from libpodcasts import HTTPAuthError
72 from gpodder.libgpodder import gl
74 from libplayers import UserAppsReader
76 from libtagupdate import tagging_supported
78 if gpodder.interface == gpodder.GUI:
79 WEB_BROWSER_ICON = 'web-browser'
80 elif gpodder.interface == gpodder.MAEMO:
81 import hildon
82 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
84 app_name = "gpodder"
85 app_version = "unknown" # will be set in main() call
86 app_authors = [
87 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
88 '',
89 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
90 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
91 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
92 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
93 'Carlos Moffat', 'Chris Arnold', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
94 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'FFranci72', 'Florian Richter', 'Frank Harper',
95 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
96 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
97 'Jérôme Chabod', 'Jerry Moss',
98 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
99 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
100 'Jürgen Schinker', 'Justin Forest',
101 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
102 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Mykola Nikishov', 'narf',
103 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
104 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
105 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
106 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
107 'Robert Young', 'Roel Groeneveld',
108 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
109 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
110 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
111 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
112 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
114 'List may be incomplete - please contact me.'
116 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
117 app_website = 'http://www.gpodder.org/'
119 # these will be filled with pathnames in bin/gpodder
120 glade_dir = [ 'share', 'gpodder' ]
121 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
122 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
125 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
126 gpodder_main_window = None
127 finger_friendly_widgets = []
129 def __init__( self, **kwargs):
130 path = os.path.join( glade_dir, '%s.glade' % app_name)
131 root = self.__class__.__name__
132 domain = app_name
134 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
136 # Set widgets to finger-friendly mode if on Maemo
137 for widget_name in self.finger_friendly_widgets:
138 self.set_finger_friendly(getattr(self, widget_name))
140 if root == 'gPodder':
141 GladeWidget.gpodder_main_window = self.gPodder
142 else:
143 # If we have a child window, set it transient for our main window
144 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
146 if gpodder.interface == gpodder.GUI:
147 if hasattr( self, 'center_on_widget'):
148 ( x, y ) = self.gpodder_main_window.get_position()
149 a = self.center_on_widget.allocation
150 ( x, y ) = ( x + a.x, y + a.y )
151 ( w, h ) = ( a.width, a.height )
152 ( pw, ph ) = getattr( self, root).get_size()
153 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
154 else:
155 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
157 def notification(self, message, title=None):
158 util.idle_add(self.show_message, message, title)
160 def show_message( self, message, title = None):
161 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
162 if title is None:
163 title = 'gPodder'
164 self.tray_icon.send_notification(message, title)
165 return
167 if gpodder.interface == gpodder.GUI:
168 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
169 if title:
170 dlg.set_title(str(title))
171 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
172 else:
173 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
174 elif gpodder.interface == gpodder.MAEMO:
175 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
177 dlg.run()
178 dlg.destroy()
180 def set_finger_friendly(self, widget):
182 If we are on Maemo, we carry out the necessary
183 operations to turn a widget into a finger-friendly
184 one, depending on which type of widget it is (i.e.
185 buttons will have more padding, TreeViews a thick
186 scrollbar, etc..)
188 if gpodder.interface == gpodder.MAEMO:
189 if isinstance(widget, gtk.Misc):
190 widget.set_padding(0, 5)
191 elif isinstance(widget, gtk.Button):
192 for child in widget.get_children():
193 if isinstance(child, gtk.Alignment):
194 child.set_padding(10, 10, 5, 5)
195 else:
196 child.set_padding(10, 10)
197 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
198 parent = widget.get_parent()
199 if isinstance(parent, gtk.ScrolledWindow):
200 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
201 elif isinstance(widget, gtk.MenuItem):
202 for child in widget.get_children():
203 self.set_finger_friendly(child)
204 else:
205 log('Cannot set widget finger-friendly: %s', widget, sender=self)
207 return widget
209 def show_confirmation( self, message, title = None):
210 if gpodder.interface == gpodder.GUI:
211 affirmative = gtk.RESPONSE_YES
212 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
213 if title:
214 dlg.set_title(str(title))
215 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
216 else:
217 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
218 elif gpodder.interface == gpodder.MAEMO:
219 affirmative = gtk.RESPONSE_OK
220 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
222 response = dlg.run()
223 dlg.destroy()
225 return response == affirmative
227 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
228 """ An authentication dialog based on
229 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
231 dialog = gtk.MessageDialog(
232 GladeWidget.gpodder_main_window,
233 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
234 gtk.MESSAGE_QUESTION,
235 gtk.BUTTONS_OK_CANCEL )
237 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
239 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
240 dialog.set_title(_('Authentication required'))
241 dialog.format_secondary_markup(message)
242 dialog.set_default_response(gtk.RESPONSE_OK)
244 if register_callback is not None:
245 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
247 username_entry = gtk.Entry()
248 password_entry = gtk.Entry()
250 username_entry.connect('activate', lambda w: password_entry.grab_focus())
251 password_entry.set_visibility(False)
252 password_entry.set_activates_default(True)
254 if username is not None:
255 username_entry.set_text(username)
256 if password is not None:
257 password_entry.set_text(password)
259 table = gtk.Table(2, 2)
260 table.set_row_spacings(6)
261 table.set_col_spacings(6)
263 username_label = gtk.Label()
264 username_label.set_markup('<b>' + username_prompt + ':</b>')
265 username_label.set_alignment(0.0, 0.5)
266 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
267 table.attach(username_entry, 1, 2, 0, 1)
269 password_label = gtk.Label()
270 password_label.set_markup('<b>' + _('Password') + ':</b>')
271 password_label.set_alignment(0.0, 0.5)
272 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
273 table.attach(password_entry, 1, 2, 1, 2)
275 dialog.vbox.pack_end(table, True, True, 0)
276 dialog.show_all()
277 response = dialog.run()
279 while response == gtk.RESPONSE_HELP:
280 register_callback()
281 response = dialog.run()
283 password_entry.set_visibility(True)
284 dialog.destroy()
286 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
288 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
289 if dst_filename is None:
290 dst_filename = src_filename
292 if dst_directory is None:
293 dst_directory = os.path.expanduser( '~')
295 ( base, extension ) = os.path.splitext( src_filename)
297 if not dst_filename.endswith( extension):
298 dst_filename += extension
300 if gpodder.interface == gpodder.GUI:
301 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
302 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
303 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
304 elif gpodder.interface == gpodder.MAEMO:
305 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
307 dlg.set_do_overwrite_confirmation( True)
308 dlg.set_current_name( os.path.basename( dst_filename))
309 dlg.set_current_folder( dst_directory)
311 result = False
312 folder = dst_directory
313 if dlg.run() == gtk.RESPONSE_OK:
314 result = True
315 dst_filename = dlg.get_filename()
316 folder = dlg.get_current_folder()
317 if not dst_filename.endswith( extension):
318 dst_filename += extension
320 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
322 try:
323 shutil.copyfile( src_filename, dst_filename)
324 except:
325 log( 'Error copying file.', sender = self, traceback = True)
327 dlg.destroy()
328 return (result, folder)
331 class gPodder(GladeWidget):
332 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
333 ENTER_URL_TEXT = _('Enter podcast URL...')
335 def new(self):
336 if gpodder.interface == gpodder.MAEMO:
337 # Maemo-specific changes to the UI
338 global scalable_dir
339 scalable_dir = scalable_dir.replace('.svg', '.png')
341 self.app = hildon.Program()
342 gtk.set_application_name('gPodder')
343 self.window = hildon.Window()
344 self.window.connect('delete-event', self.on_gPodder_delete_event)
345 self.window.connect('window-state-event', self.window_state_event)
347 self.itemUpdateChannel.show()
348 self.UpdateChannelSeparator.show()
350 # Give toolbar to the hildon window
351 self.toolbar.parent.remove(self.toolbar)
352 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
353 self.window.add_toolbar(self.toolbar)
355 self.app.add_window(self.window)
356 self.vMain.reparent(self.window)
357 self.gPodder = self.window
359 # Reparent the main menu
360 menu = gtk.Menu()
361 for child in self.mainMenu.get_children():
362 child.reparent(menu)
363 self.itemQuit.reparent(menu)
364 self.window.set_menu(menu)
366 self.mainMenu.destroy()
367 self.window.show()
369 # do some widget hiding
370 self.toolbar.remove(self.toolTransfer)
371 self.itemTransferSelected.hide_all()
372 self.item_email_subscriptions.hide_all()
374 # Feed cache update button
375 self.label120.set_text(_('Update'))
377 # get screen real estate
378 self.hboxContainer.set_border_width(0)
380 self.gPodder.connect('key-press-event', self.on_key_press)
381 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
383 if gl.config.show_url_entry_in_podcast_list:
384 self.hboxAddChannel.show()
386 if not gl.config.show_toolbar:
387 self.toolbar.hide_all()
389 gl.config.add_observer(self.on_config_changed)
390 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
391 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
392 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
393 self.entry_add_channel_unfocus(self.entryAddChannel, None)
395 self.uar = None
396 self.tray_icon = None
398 self.fullscreen = False
399 self.minimized = False
400 self.gPodder.connect('window-state-event', self.window_state_event)
402 self.already_notified_new_episodes = []
403 self.show_hide_tray_icon()
404 self.no_episode_selected.set_sensitive(False)
406 self.itemShowToolbar.set_active(gl.config.show_toolbar)
407 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
409 gl.config.connect_gtk_window(self.gPodder, 'main_window')
410 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
412 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
413 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
414 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
415 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
417 # Make sure we free/close the download queue when we
418 # update the "max downloads" spin button
419 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
420 self.spinMaxDownloads.connect('value-changed', changed_cb)
422 self.default_title = None
423 if app_version.rfind('git') != -1:
424 self.set_title('gPodder %s' % app_version)
425 else:
426 title = self.gPodder.get_title()
427 if title is not None:
428 self.set_title(title)
429 else:
430 self.set_title(_('gPodder'))
432 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
434 # cell renderers for channel tree
435 iconcolumn = gtk.TreeViewColumn('')
437 iconcell = gtk.CellRendererPixbuf()
438 iconcolumn.pack_start( iconcell, False)
439 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
440 self.cell_channel_icon = iconcell
442 namecolumn = gtk.TreeViewColumn('')
443 namecell = gtk.CellRendererText()
444 namecell.set_property('foreground-set', True)
445 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
446 namecolumn.pack_start( namecell, True)
447 namecolumn.add_attribute( namecell, 'markup', 2)
448 namecolumn.add_attribute( namecell, 'foreground', 8)
450 iconcell = gtk.CellRendererPixbuf()
451 iconcell.set_property('xalign', 1.0)
452 namecolumn.pack_start( iconcell, False)
453 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
454 namecolumn.add_attribute(iconcell, 'visible', 7)
455 self.cell_channel_pill = iconcell
457 self.treeChannels.append_column(iconcolumn)
458 self.treeChannels.append_column(namecolumn)
459 self.treeChannels.set_headers_visible(False)
461 # enable alternating colors hint
462 self.treeAvailable.set_rules_hint( True)
463 self.treeChannels.set_rules_hint( True)
465 # connect to tooltip signals
466 try:
467 self.treeChannels.set_property('has-tooltip', True)
468 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
469 self.treeAvailable.set_property('has-tooltip', True)
470 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
471 except:
472 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
473 self.last_tooltip_channel = None
474 self.last_tooltip_episode = None
475 self.podcast_list_can_tooltip = True
476 self.episode_list_can_tooltip = True
478 # Add our context menu to treeAvailable
479 if gpodder.interface == gpodder.MAEMO:
480 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
481 else:
482 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
483 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
485 iconcell = gtk.CellRendererPixbuf()
486 if gpodder.interface == gpodder.MAEMO:
487 status_column_label = ''
488 else:
489 status_column_label = _('Status')
490 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
492 namecell = gtk.CellRendererText()
493 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
494 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
495 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
496 namecolumn.set_expand(True)
498 sizecell = gtk.CellRendererText()
499 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
501 releasecell = gtk.CellRendererText()
502 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
504 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
505 itemcolumn.set_reorderable(True)
506 self.treeAvailable.append_column(itemcolumn)
508 if gpodder.interface == gpodder.MAEMO:
509 # Due to screen space contraints, we
510 # hide these columns here by default
511 self.column_size = sizecolumn
512 self.column_released = releasecolumn
513 self.column_released.set_visible(False)
514 self.column_size.set_visible(False)
516 # enable search in treeavailable
517 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
519 # enable multiple selection support
520 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
521 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
523 # columns and renderers for "download progress" tab
524 episodecell = gtk.CellRendererText()
525 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
526 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
527 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
528 episodecolumn.set_expand(True)
530 speedcell = gtk.CellRendererText()
531 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
533 progresscell = gtk.CellRendererProgress()
534 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
535 progresscolumn.set_expand(True)
537 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
538 self.treeDownloads.append_column( itemcolumn)
540 # After we've set up most of the window, show it :)
541 if not gpodder.interface == gpodder.MAEMO:
542 self.gPodder.show()
544 if self.tray_icon:
545 if gl.config.start_iconified:
546 self.iconify_main_window()
547 elif gl.config.minimize_to_tray:
548 self.tray_icon.set_visible(False)
550 # a dictionary that maps episode URLs to the current
551 # treeAvailable row numbers to generate tree paths
552 self.url_path_mapping = {}
554 # a dictionary that maps channel URLs to the current
555 # treeChannels row numbers to generate tree paths
556 self.channel_url_path_mapping = {}
558 services.download_status_manager.register( 'list-changed', self.download_status_updated)
559 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
560 services.cover_downloader.register('cover-available', self.cover_download_finished)
561 services.cover_downloader.register('cover-removed', self.cover_file_removed)
562 self.cover_cache = {}
564 self.treeDownloads.set_model( services.download_status_manager.tree_model)
566 #Add Drag and Drop Support
567 flags = gtk.DEST_DEFAULT_ALL
568 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
569 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
570 self.treeChannels.drag_dest_set( flags, targets, actions)
571 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
573 # Subscribed channels
574 self.active_channel = None
575 self.channels = load_channels()
576 self.channel_list_changed = True
577 self.update_podcasts_tab()
579 # load list of user applications for audio playback
580 self.user_apps_reader = UserAppsReader(['audio', 'video'])
581 Thread(target=self.read_apps).start()
583 # Clean up old, orphaned download files
584 gl.clean_up_downloads( delete_partial = True)
586 # Set the "Device" menu item for the first time
587 self.update_item_device()
589 # Last folder used for saving episodes
590 self.folder_for_saving_episodes = None
592 # Set up default channel colors
593 self.channel_colors = {
594 'default': None,
595 'updating': gl.config.color_updating_feeds,
596 'parse_error': '#ff0000',
599 # Now, update the feed cache, when everything's in place
600 self.btnUpdateFeeds.show_all()
601 self.updated_feeds = 0
602 self.updating_feed_cache = False
603 self.feed_cache_update_cancelled = False
604 self.update_feed_cache(force_update=gl.config.update_on_startup)
606 # Start the auto-update procedure
607 self.auto_update_procedure(first_run=True)
609 # Delete old episodes if the user wishes to
610 if gl.config.auto_remove_old_episodes:
611 old_episodes = self.get_old_episodes()
612 if len(old_episodes) > 0:
613 self.delete_episode_list(old_episodes, confirm=False)
614 self.updateComboBox()
616 # First-time users should be asked if they want to see the OPML
617 if len(self.channels) == 0:
618 util.idle_add(self.on_itemUpdate_activate, None)
620 def on_tree_channels_resize(self, widget, allocation):
621 if not gl.config.podcast_sidebar_save_space:
622 return
624 window_allocation = self.gPodder.get_allocation()
625 percentage = 100. * float(allocation.width) / float(window_allocation.width)
626 if hasattr(self, 'cell_channel_icon'):
627 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
628 if hasattr(self, 'cell_channel_pill'):
629 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
631 def entry_add_channel_focus(self, widget, event):
632 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
633 if widget.get_text() == self.ENTER_URL_TEXT:
634 widget.set_text('')
636 def entry_add_channel_unfocus(self, widget, event):
637 if widget.get_text() == '':
638 widget.set_text(self.ENTER_URL_TEXT)
639 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
641 def on_config_changed(self, name, old_value, new_value):
642 if name == 'show_toolbar':
643 if new_value:
644 self.toolbar.show()
645 else:
646 self.toolbar.hide()
647 elif name == 'episode_list_descriptions':
648 self.updateTreeView()
649 elif name == 'show_url_entry_in_podcast_list':
650 if new_value:
651 self.hboxAddChannel.show()
652 else:
653 self.hboxAddChannel.hide()
655 def read_apps(self):
656 time.sleep(3) # give other parts of gpodder a chance to start up
657 self.user_apps_reader.read()
658 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
659 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
661 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
662 # With get_bin_window, we get the window that contains the rows without
663 # the header. The Y coordinate of this window will be the height of the
664 # treeview header. This is the amount we have to subtract from the
665 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
666 (x_bin, y_bin) = treeview.get_bin_window().get_position()
667 y -= x_bin
668 y -= y_bin
669 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
671 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
672 self.last_tooltip_episode = None
673 return False
675 if path is not None:
676 model = treeview.get_model()
677 iter = model.get_iter(path)
678 url = model.get_value(iter, 0)
679 description = model.get_value(iter, 7)
680 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
681 self.last_tooltip_episode = None
682 return False
683 self.last_tooltip_episode = url
685 if len(description) > 400:
686 description = description[:398]+'[...]'
688 tooltip.set_text(description)
689 return True
691 self.last_tooltip_episode = None
692 return False
694 def podcast_list_allow_tooltips(self):
695 self.podcast_list_can_tooltip = True
697 def episode_list_allow_tooltips(self):
698 self.episode_list_can_tooltip = True
700 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
701 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
703 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
704 self.last_tooltip_channel = None
705 return False
707 if path is not None:
708 model = treeview.get_model()
709 iter = model.get_iter(path)
710 url = model.get_value(iter, 0)
711 for channel in self.channels:
712 if channel.url == url:
713 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
714 self.last_tooltip_channel = None
715 return False
716 self.last_tooltip_channel = channel
717 channel.request_save_dir_size()
718 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
719 error_str = model.get_value(iter, 6)
720 if error_str:
721 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
722 error_str = '<span foreground="#ff0000">%s</span>' % error_str
723 table = gtk.Table(rows=3, columns=3)
724 table.set_row_spacings(5)
725 table.set_col_spacings(5)
726 table.set_border_width(5)
728 heading = gtk.Label()
729 heading.set_alignment(0, 1)
730 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
731 table.attach(heading, 0, 1, 0, 1)
732 size_info = gtk.Label()
733 size_info.set_alignment(1, 1)
734 size_info.set_justify(gtk.JUSTIFY_RIGHT)
735 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
736 table.attach(size_info, 2, 3, 0, 1)
738 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
740 if len(channel.description) < 500:
741 description = channel.description
742 else:
743 pos = channel.description.find('\n\n')
744 if pos == -1 or pos > 500:
745 description = channel.description[:498]+'[...]'
746 else:
747 description = channel.description[:pos]
749 description = gtk.Label(description)
750 if error_str:
751 description.set_markup(error_str)
752 description.set_alignment(0, 0)
753 description.set_line_wrap(True)
754 table.attach(description, 0, 3, 2, 3)
756 table.show_all()
757 tooltip.set_custom(table)
759 return True
761 self.last_tooltip_channel = None
762 return False
764 def update_m3u_playlist_clicked(self, widget):
765 self.active_channel.update_m3u_playlist()
766 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
768 def treeview_channels_button_pressed( self, treeview, event):
769 global WEB_BROWSER_ICON
771 if event.button == 3:
772 ( x, y ) = ( int(event.x), int(event.y) )
773 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
775 paths = []
777 # Did the user right-click into a selection?
778 selection = treeview.get_selection()
779 if selection.count_selected_rows() and path:
780 ( model, paths ) = selection.get_selected_rows()
781 if path not in paths:
782 # We have right-clicked, but not into the
783 # selection, assume we don't want to operate
784 # on the selection
785 paths = []
787 # No selection or right click not in selection:
788 # Select the single item where we clicked
789 if not len( paths) and path:
790 treeview.grab_focus()
791 treeview.set_cursor( path, column, 0)
793 ( model, paths ) = ( treeview.get_model(), [ path ] )
795 # We did not find a selection, and the user didn't
796 # click on an item to select -- don't show the menu
797 if not len( paths):
798 return True
800 menu = gtk.Menu()
802 item = gtk.ImageMenuItem( _('Open download folder'))
803 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
804 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
805 menu.append( item)
807 item = gtk.ImageMenuItem( _('Update Feed'))
808 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
809 item.connect('activate', self.on_itemUpdateChannel_activate )
810 item.set_sensitive( not self.updating_feed_cache )
811 menu.append( item)
813 if gl.config.create_m3u_playlists:
814 item = gtk.ImageMenuItem(_('Update M3U playlist'))
815 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
816 item.connect('activate', self.update_m3u_playlist_clicked)
817 menu.append(item)
819 if self.active_channel.link:
820 item = gtk.ImageMenuItem(_('Visit website'))
821 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
822 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
823 menu.append(item)
825 if self.active_channel.channel_is_locked:
826 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
827 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
828 item.connect('activate', self.on_channel_toggle_lock_activate)
829 menu.append(self.set_finger_friendly(item))
830 else:
831 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
832 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
833 item.connect('activate', self.on_channel_toggle_lock_activate)
834 menu.append(self.set_finger_friendly(item))
837 menu.append( gtk.SeparatorMenuItem())
839 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
840 item.connect( 'activate', self.on_itemEditChannel_activate)
841 menu.append( item)
843 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
844 item.connect( 'activate', self.on_itemRemoveChannel_activate)
845 menu.append( item)
847 menu.show_all()
848 # Disable tooltips while we are showing the menu, so
849 # the tooltip will not appear over the menu
850 self.podcast_list_can_tooltip = False
851 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
852 menu.popup( None, None, None, event.button, event.time)
854 return True
856 def on_itemClose_activate(self, widget):
857 if self.tray_icon is not None:
858 if gpodder.interface == gpodder.MAEMO:
859 self.gPodder.set_property('visible', False)
860 else:
861 self.iconify_main_window()
862 else:
863 self.on_gPodder_delete_event(widget)
865 def cover_file_removed(self, channel_url):
867 The Cover Downloader calls this when a previously-
868 available cover has been removed from the disk. We
869 have to update our cache to reflect this change.
871 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
872 for row in self.treeChannels.get_model():
873 if row[COLUMN_URL] == channel_url:
874 row[COLUMN_PIXBUF] = None
875 key = (channel_url, gl.config.podcast_list_icon_size, \
876 gl.config.podcast_list_icon_size)
877 if key in self.cover_cache:
878 del self.cover_cache[key]
881 def cover_download_finished(self, channel_url, pixbuf):
883 The Cover Downloader calls this when it has finished
884 downloading (or registering, if already downloaded)
885 a new channel cover, which is ready for displaying.
887 if pixbuf is not None:
888 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
889 for row in self.treeChannels.get_model():
890 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
891 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)
892 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
894 def save_episode_as_file( self, url, *args):
895 episode = self.active_channel.find_episode(url)
897 folder = self.folder_for_saving_episodes
898 (result, folder) = self.show_copy_dialog(src_filename=episode.local_filename(), dst_filename=episode.sync_filename(), dst_directory=folder)
899 self.folder_for_saving_episodes = folder
901 def copy_episode_bluetooth(self, url, *args):
902 episode = self.active_channel.find_episode(url)
903 filename = episode.local_filename()
905 if gl.config.bluetooth_use_device_address:
906 device = gl.config.bluetooth_device_address
907 else:
908 device = None
910 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
911 (base, ext) = os.path.splitext(filename)
912 if not destfile.endswith(ext):
913 destfile += ext
915 if gl.config.bluetooth_use_converter:
916 title = _('Converting file')
917 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
918 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
919 dlg.set_title(title)
920 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
921 dlg.show_all()
922 else:
923 dlg = None
925 def convert_and_send_thread(filename, destfile, device, dialog, notify):
926 if gl.config.bluetooth_use_converter:
927 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
928 result = p.wait()
929 if dialog is not None:
930 dialog.destroy()
931 else:
932 try:
933 shutil.copyfile(filename, destfile)
934 result = 0
935 except:
936 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
937 result = 1
939 if result == 0 or not os.path.exists(destfile):
940 util.bluetooth_send_file(destfile, device)
941 else:
942 notify(_('Error converting file.'), _('Bluetooth file transfer'))
943 util.delete_file(destfile)
945 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
947 def treeview_button_pressed( self, treeview, event):
948 global WEB_BROWSER_ICON
950 # Use right-click for the Desktop version and left-click for Maemo
951 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
952 (event.button == 3 and gpodder.interface == gpodder.GUI):
953 ( x, y ) = ( int(event.x), int(event.y) )
954 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
956 paths = []
958 # Did the user right-click into a selection?
959 selection = self.treeAvailable.get_selection()
960 if selection.count_selected_rows() and path:
961 ( model, paths ) = selection.get_selected_rows()
962 if path not in paths:
963 # We have right-clicked, but not into the
964 # selection, assume we don't want to operate
965 # on the selection
966 paths = []
968 # No selection or right click not in selection:
969 # Select the single item where we clicked
970 if not len( paths) and path:
971 treeview.grab_focus()
972 treeview.set_cursor( path, column, 0)
974 ( model, paths ) = ( treeview.get_model(), [ path ] )
976 # We did not find a selection, and the user didn't
977 # click on an item to select -- don't show the menu
978 if not len( paths):
979 return True
981 first_url = model.get_value( model.get_iter( paths[0]), 0)
982 episode = db.load_episode(first_url)
984 menu = gtk.Menu()
986 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
988 if can_play:
989 if open_instead_of_play:
990 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
991 else:
992 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
993 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
994 menu.append(self.set_finger_friendly(item))
996 if not episode['is_locked'] and can_delete:
997 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
998 item.connect('activate', self.on_btnDownloadedDelete_clicked)
999 menu.append(self.set_finger_friendly(item))
1001 if can_cancel:
1002 item = gtk.ImageMenuItem( _('Cancel download'))
1003 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
1004 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
1005 menu.append(self.set_finger_friendly(item))
1007 if can_download:
1008 item = gtk.ImageMenuItem(_('Download'))
1009 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1010 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
1011 menu.append(self.set_finger_friendly(item))
1013 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1014 item = gtk.ImageMenuItem(_('Do not download'))
1015 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1016 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1017 menu.append(self.set_finger_friendly(item))
1018 elif episode['state'] == db.STATE_NORMAL and can_download:
1019 item = gtk.ImageMenuItem(_('Mark as new'))
1020 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1021 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1022 menu.append(self.set_finger_friendly(item))
1024 if can_play and not can_download:
1025 menu.append( gtk.SeparatorMenuItem())
1026 item = gtk.ImageMenuItem(_('Save to disk'))
1027 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1028 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1029 menu.append(self.set_finger_friendly(item))
1030 if gl.bluetooth_available:
1031 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1032 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1033 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1034 menu.append(self.set_finger_friendly(item))
1035 if can_transfer:
1036 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1037 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1038 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1039 menu.append(self.set_finger_friendly(item))
1041 if can_play:
1042 menu.append( gtk.SeparatorMenuItem())
1043 is_played = episode['is_played']
1044 if is_played:
1045 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1046 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1047 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1048 menu.append(self.set_finger_friendly(item))
1049 else:
1050 item = gtk.ImageMenuItem(_('Mark as played'))
1051 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1052 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1053 menu.append(self.set_finger_friendly(item))
1055 is_locked = episode['is_locked']
1056 if is_locked:
1057 item = gtk.ImageMenuItem(_('Allow deletion'))
1058 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1059 item.connect('activate', self.on_item_toggle_lock_activate)
1060 menu.append(self.set_finger_friendly(item))
1061 else:
1062 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1063 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1064 item.connect('activate', self.on_item_toggle_lock_activate)
1065 menu.append(self.set_finger_friendly(item))
1067 if len(paths) == 1:
1068 menu.append(gtk.SeparatorMenuItem())
1069 # Single item, add episode information menu item
1070 episode_url = model.get_value( model.get_iter( paths[0]), 0)
1071 item = gtk.ImageMenuItem(_('Episode details'))
1072 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1073 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1074 menu.append(self.set_finger_friendly(item))
1075 episode = self.active_channel.find_episode(episode_url)
1076 # If we have it, also add episode website link
1077 if episode and episode.link and episode.link != episode.url:
1078 item = gtk.ImageMenuItem(_('Visit website'))
1079 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1080 item.connect('activate', lambda w: util.open_website(episode.link))
1081 menu.append(self.set_finger_friendly(item))
1083 if gpodder.interface == gpodder.MAEMO:
1084 # Because we open the popup on left-click for Maemo,
1085 # we also include a non-action to close the menu
1086 menu.append(gtk.SeparatorMenuItem())
1087 item = gtk.ImageMenuItem(_('Close this menu'))
1088 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1089 menu.append(self.set_finger_friendly(item))
1091 menu.show_all()
1092 # Disable tooltips while we are showing the menu, so
1093 # the tooltip will not appear over the menu
1094 self.episode_list_can_tooltip = False
1095 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1096 menu.popup( None, None, None, event.button, event.time)
1098 return True
1100 def set_title(self, new_title):
1101 self.default_title = new_title
1102 self.gPodder.set_title(new_title)
1104 def download_progress_updated( self, count, percentage):
1105 title = [ self.default_title ]
1107 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1109 if count == 1:
1110 title.append( _('downloading one file'))
1111 elif count > 1:
1112 title.append( _('downloading %d files') % count)
1114 if len(title) == 2:
1115 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1117 self.gPodder.set_title( ' - '.join( title))
1119 # Have all the downloads completed?
1120 # If so execute user command if defined, else do nothing
1121 if count == 0:
1122 if len(gl.config.cmd_all_downloads_complete) > 0:
1123 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1125 def update_selected_episode_list_icons(self):
1127 Updates the status icons in the episode list
1129 selection = self.treeAvailable.get_selection()
1130 (model, paths) = selection.get_selected_rows()
1131 for path in paths:
1132 iter = model.get_iter(path)
1133 self.active_channel.iter_set_downloading_columns(model, iter)
1135 def playback_episode(self, episode, stream=False):
1136 (success, application) = gl.playback_episode(episode, stream)
1137 if not success:
1138 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), ))
1139 self.update_selected_episode_list_icons()
1140 self.updateComboBox(only_selected_channel=True)
1142 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1143 if model is None:
1144 return True
1146 key = key.lower()
1148 # columns, as defined in libpodcasts' get model method
1149 # 1 = episode title, 7 = description
1150 columns = (1, 7)
1152 for column in columns:
1153 value = model.get_value( iter, column).lower()
1154 if value.find( key) != -1:
1155 return False
1157 return True
1159 def change_menu_item(self, menuitem, icon=None, label=None):
1160 if icon is not None:
1161 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1162 if label is not None:
1163 label_widget = menuitem.get_child()
1164 label_widget.set_text(label)
1166 def play_or_download(self):
1167 if self.wNotebook.get_current_page() > 0:
1168 return
1170 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1171 ( is_played, is_locked ) = (False,)*2
1173 open_instead_of_play = False
1175 selection = self.treeAvailable.get_selection()
1176 if selection.count_selected_rows() > 0:
1177 (model, paths) = selection.get_selected_rows()
1179 for path in paths:
1180 url = model.get_value( model.get_iter( path), 0)
1181 local_filename = model.get_value( model.get_iter( path), 8)
1183 episode = podcastItem.load(url, self.active_channel)
1185 if episode.file_type() not in ('audio', 'video'):
1186 open_instead_of_play = True
1188 if episode.was_downloaded():
1189 can_play = episode.was_downloaded(and_exists=True)
1190 can_delete = True
1191 is_played = episode.is_played
1192 is_locked = episode.is_locked
1193 if not can_play:
1194 can_download = True
1195 else:
1196 if services.download_status_manager.is_download_in_progress(url):
1197 can_cancel = True
1198 else:
1199 can_download = True
1201 can_download = can_download and not can_cancel
1202 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1203 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1205 if open_instead_of_play:
1206 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1207 can_transfer = False
1208 else:
1209 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1211 self.toolPlay.set_sensitive( can_play)
1212 self.toolDownload.set_sensitive( can_download)
1213 self.toolTransfer.set_sensitive( can_transfer)
1214 self.toolCancel.set_sensitive( can_cancel)
1216 if can_cancel:
1217 self.item_cancel_download.show_all()
1218 else:
1219 self.item_cancel_download.hide_all()
1220 if can_download:
1221 self.itemDownloadSelected.show_all()
1222 else:
1223 self.itemDownloadSelected.hide_all()
1224 if can_play:
1225 if open_instead_of_play:
1226 self.itemOpenSelected.show_all()
1227 self.itemPlaySelected.hide_all()
1228 else:
1229 self.itemPlaySelected.show_all()
1230 self.itemOpenSelected.hide_all()
1231 if not can_download:
1232 self.itemDeleteSelected.show_all()
1233 else:
1234 self.itemDeleteSelected.hide_all()
1235 self.item_toggle_played.show_all()
1236 self.item_toggle_lock.show_all()
1237 self.separator9.show_all()
1238 if is_played:
1239 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1240 else:
1241 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1242 if is_locked:
1243 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1244 else:
1245 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1246 else:
1247 self.itemPlaySelected.hide_all()
1248 self.itemOpenSelected.hide_all()
1249 self.itemDeleteSelected.hide_all()
1250 self.item_toggle_played.hide_all()
1251 self.item_toggle_lock.hide_all()
1252 self.separator9.hide_all()
1253 if can_play or can_download or can_cancel:
1254 self.item_episode_details.show_all()
1255 self.separator16.show_all()
1256 self.no_episode_selected.hide_all()
1257 else:
1258 self.item_episode_details.hide_all()
1259 self.separator16.hide_all()
1260 self.no_episode_selected.show_all()
1262 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1264 def download_status_updated(self, episode_urls, channel_urls):
1265 count = services.download_status_manager.count()
1266 if count:
1267 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1268 else:
1269 self.labelDownloads.set_text( _('Downloads'))
1271 model = self.treeAvailable.get_model()
1272 for url in episode_urls:
1273 if url in self.url_path_mapping:
1274 path = (self.url_path_mapping[url],)
1275 self.active_channel.iter_set_downloading_columns(model, model.get_iter(path))
1277 self.updateComboBox(only_these_urls=channel_urls)
1279 def on_cbMaxDownloads_toggled(self, widget, *args):
1280 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1282 def on_cbLimitDownloads_toggled(self, widget, *args):
1283 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1285 def episode_new_status_changed(self):
1286 self.updateComboBox()
1287 self.updateTreeView()
1289 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1290 selection = self.treeChannels.get_selection()
1291 (model, iter) = selection.get_selected()
1293 if only_selected_channel:
1294 # very cheap! only update selected channel
1295 if iter and self.active_channel is not None:
1296 update_channel_model_by_iter(model, iter,
1297 self.active_channel, self.channel_colors,
1298 self.cover_cache,
1299 gl.config.podcast_list_icon_size,
1300 gl.config.podcast_list_icon_size)
1301 elif not self.channel_list_changed:
1302 # we can keep the model, but have to update some
1303 if only_these_urls is None:
1304 # still cheaper than reloading the whole list
1305 iter = model.get_iter_first()
1306 while iter is not None:
1307 (index,) = model.get_path(iter)
1308 update_channel_model_by_iter(model, iter,
1309 self.channels[index], self.channel_colors,
1310 self.cover_cache,
1311 gl.config.podcast_list_icon_size,
1312 gl.config.podcast_list_icon_size)
1313 iter = model.iter_next(iter)
1314 else:
1315 # ok, we got a bunch of urls to update
1316 for url in only_these_urls:
1317 index = self.channel_url_path_mapping[url]
1318 path = (index,)
1319 iter = model.get_iter(path)
1320 update_channel_model_by_iter(model, iter,
1321 self.channels[index], self.channel_colors,
1322 self.cover_cache,
1323 gl.config.podcast_list_icon_size,
1324 gl.config.podcast_list_icon_size)
1325 else:
1326 if model and iter and selected_url is None:
1327 # Get the URL of the currently-selected podcast
1328 selected_url = model.get_value(iter, 0)
1330 (model, urls) = channels_to_model(self.channels,
1331 self.channel_colors, self.cover_cache,
1332 gl.config.podcast_list_icon_size,
1333 gl.config.podcast_list_icon_size)
1335 self.channel_url_path_mapping = dict(zip(urls, range(len(urls))))
1336 self.treeChannels.set_model(model)
1338 try:
1339 selected_path = (0,)
1340 # Find the previously-selected URL in the new
1341 # model if we have an URL (else select first)
1342 if selected_url is not None:
1343 pos = model.get_iter_first()
1344 while pos is not None:
1345 url = model.get_value(pos, 0)
1346 if url == selected_url:
1347 selected_path = model.get_path(pos)
1348 break
1349 pos = model.iter_next(pos)
1351 self.treeChannels.get_selection().select_path(selected_path)
1352 except:
1353 log( 'Cannot set selection on treeChannels', sender = self)
1354 self.on_treeChannels_cursor_changed( self.treeChannels)
1355 self.channel_list_changed = False
1357 def updateTreeView(self):
1358 if self.channels and self.active_channel is not None:
1359 (model, urls) = self.active_channel.get_tree_model()
1360 self.treeAvailable.set_model(model)
1361 self.url_path_mapping = dict(zip(urls, range(len(urls))))
1362 self.treeAvailable.columns_autosize()
1363 self.play_or_download()
1364 else:
1365 model = self.treeAvailable.get_model()
1366 if model is not None:
1367 model.clear()
1369 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1370 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1372 dnd_channel = None
1373 if path is not None:
1374 model = self.treeChannels.get_model()
1375 iter = model.get_iter(path)
1376 url = model.get_value(iter, 0)
1377 for channel in self.channels:
1378 if channel.url == url:
1379 dnd_channel = channel
1380 break
1382 result = sel.data
1383 rl = result.strip().lower()
1384 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1385 services.cover_downloader.replace_cover(dnd_channel, result)
1386 else:
1387 self.add_new_channel(result)
1389 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1390 result = util.normalize_feed_url( result)
1392 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1393 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1394 waitdlg.set_title(_('Downloading episode list'))
1395 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1396 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1397 waitpb = gtk.ProgressBar()
1398 if block:
1399 waitdlg.vbox.add(waitpb)
1400 waitdlg.show_all()
1401 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1403 if not result:
1404 title = _('URL scheme not supported')
1405 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1406 self.show_message( message, title)
1407 return
1409 for old_channel in self.channels:
1410 if old_channel.url == result:
1411 log( 'Channel already exists: %s', result)
1412 # Select the existing channel in combo box
1413 for i in range( len( self.channels)):
1414 if self.channels[i] == old_channel:
1415 self.treeChannels.get_selection().select_path( (i,))
1416 self.on_treeChannels_cursor_changed(self.treeChannels)
1417 break
1418 self.show_message( _('You have already subscribed to this podcast: %s') % (
1419 saxutils.escape( old_channel.title), ), _('Already added'))
1420 waitdlg.destroy()
1421 return
1423 self.entryAddChannel.set_text(_('Downloading feed...'))
1424 self.entryAddChannel.set_sensitive(False)
1425 self.btnAddChannel.set_sensitive(False)
1426 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1427 thread = Thread( target=self.add_new_channel_proc, args=args )
1428 thread.start()
1430 while block and thread.isAlive():
1431 while gtk.events_pending():
1432 gtk.main_iteration( False)
1433 waitpb.pulse()
1434 time.sleep(0.05)
1437 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1438 log( 'Adding new channel: %s', url)
1439 channel = error = None
1440 try:
1441 channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1442 except HTTPAuthError, e:
1443 error = e
1444 except Exception, e:
1445 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1447 util.idle_add( callback, channel, url, error, *callback_args )
1449 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1450 if channel is not None:
1451 self.channels.append( channel)
1452 self.channel_list_changed = True
1453 save_channels( self.channels)
1454 if not quiet:
1455 # download changed channels and select the new episode in the UI afterwards
1456 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1458 (username, password) = util.username_password_from_url( url)
1459 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')):
1460 channel.username = username
1461 channel.password = password
1462 log('Saving authentication data for episode downloads..', sender = self)
1463 channel.save()
1464 # We need to update the channel list otherwise the authentication
1465 # data won't show up in the channel editor.
1466 # TODO: Only updated the newly added feed to save some cpu cycles
1467 self.channels = load_channels()
1468 self.channel_list_changed = True
1470 if ask_download_new:
1471 new_episodes = channel.get_new_episodes()
1472 if len(new_episodes):
1473 self.new_episodes_show(new_episodes)
1475 elif isinstance( error, HTTPAuthError ):
1476 response, auth_tokens = self.UsernamePasswordDialog(
1477 _('Feed requires authentication'), _('Please enter your username and password.'))
1479 if response:
1480 self.add_new_channel( url, authentication_tokens=auth_tokens )
1482 else:
1483 # Ok, the URL is not a channel, or there is some other
1484 # error - let's see if it's a web page or OPML file...
1485 try:
1486 data = urllib2.urlopen(url).read().lower()
1487 if '</opml>' in data:
1488 # This looks like an OPML feed
1489 self.on_item_import_from_file_activate(None, url)
1491 elif '</html>' in data:
1492 # This looks like a web page
1493 title = _('The URL is a website')
1494 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.)')
1495 if self.show_confirmation(message, title):
1496 util.open_website(url)
1498 except Exception, e:
1499 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1501 title = _('Error adding podcast')
1502 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1503 self.show_message( message, title)
1505 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1506 self.entryAddChannel.set_sensitive(True)
1507 self.btnAddChannel.set_sensitive(True)
1508 self.update_podcasts_tab()
1509 waitdlg.destroy()
1512 def update_feed_cache_finish_callback(self, channels=None,
1513 notify_no_new_episodes=False, select_url_afterwards=None):
1515 db.commit()
1517 self.updating_feed_cache = False
1518 self.hboxUpdateFeeds.hide_all()
1519 self.btnUpdateFeeds.show_all()
1520 self.itemUpdate.set_sensitive(True)
1521 self.itemUpdateChannel.set_sensitive(True)
1523 # If we want to select a specific podcast (via its URL)
1524 # after the update, we give it to updateComboBox here to
1525 # select exactly this podcast after updating the view
1526 self.updateComboBox(selected_url=select_url_afterwards)
1528 if self.tray_icon:
1529 self.tray_icon.set_status(None)
1530 if self.minimized:
1531 new_episodes = []
1532 # look for new episodes to notify
1533 for channel in self.channels:
1534 for episode in channel.get_new_episodes():
1535 if not episode in self.already_notified_new_episodes:
1536 new_episodes.append(episode)
1537 self.already_notified_new_episodes.append(episode)
1538 # notify new episodes
1540 if len(new_episodes) == 0:
1541 if notify_no_new_episodes and self.tray_icon is not None:
1542 msg = _('No new episodes available for download')
1543 self.tray_icon.send_notification(msg)
1544 return
1545 elif len(new_episodes) == 1:
1546 title = _('gPodder has found %s') % (_('one new episode:'),)
1547 else:
1548 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1549 message = self.tray_icon.format_episode_list(new_episodes)
1551 #auto download new episodes
1552 if gl.config.auto_download_when_minimized:
1553 message += '\n<i>(%s...)</i>' % _('downloading')
1554 self.download_episode_list(new_episodes)
1555 self.tray_icon.send_notification(message, title)
1556 return
1558 # open the episodes selection dialog
1559 self.channels = load_channels()
1560 self.channel_list_changed = True
1561 self.updateComboBox()
1562 if not self.feed_cache_update_cancelled:
1563 self.download_all_new(channels=channels)
1565 def update_feed_cache_callback(self, progressbar, title, position, count):
1566 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1567 progressbar.set_text(progression)
1568 if self.tray_icon:
1569 self.tray_icon.set_status(
1570 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1571 if count > 0:
1572 progressbar.set_fraction(float(position)/float(count))
1574 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1575 callback_proc, finish_proc):
1577 semaphore.acquire()
1578 if not self.feed_cache_update_cancelled:
1579 try:
1580 channel.update()
1581 except:
1582 log('Darn SQLite LOCK!', sender=self, traceback=True)
1584 # By the time we get here the update may have already been cancelled
1585 if not self.feed_cache_update_cancelled:
1586 callback_proc(channel.title, self.updated_feeds, total_channels)
1588 self.updated_feeds += 1
1589 self.treeview_channel_set_color( channel, 'default' )
1590 channel.update_flag = False
1592 semaphore.release()
1593 if self.updated_feeds == total_channels:
1594 finish_proc()
1596 def on_btnCancelFeedUpdate_clicked(self, widget):
1597 self.pbFeedUpdate.set_text(_('Cancelling...'))
1598 self.feed_cache_update_cancelled = True
1600 def update_feed_cache(self, channels=None, force_update=True,
1601 notify_no_new_episodes=False, select_url_afterwards=None):
1603 if self.updating_feed_cache:
1604 return
1606 if not force_update:
1607 self.channels = load_channels()
1608 self.channel_list_changed = True
1609 self.updateComboBox()
1610 return
1612 self.updating_feed_cache = True
1613 self.itemUpdate.set_sensitive(False)
1614 self.itemUpdateChannel.set_sensitive(False)
1616 if self.tray_icon:
1617 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1619 if channels is None:
1620 channels = self.channels
1622 if len(channels) == 1:
1623 text = _('Updating %d feed.')
1624 else:
1625 text = _('Updating %d feeds.')
1626 self.pbFeedUpdate.set_text( text % len(channels))
1627 self.pbFeedUpdate.set_fraction(0)
1629 # let's get down to business..
1630 callback_proc = lambda title, pos, count: util.idle_add(
1631 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1632 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1633 channels, notify_no_new_episodes, select_url_afterwards )
1635 self.updated_feeds = 0
1636 self.feed_cache_update_cancelled = False
1637 self.btnUpdateFeeds.hide_all()
1638 self.hboxUpdateFeeds.show_all()
1639 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1641 for channel in channels:
1642 self.treeview_channel_set_color( channel, 'updating' )
1643 channel.update_flag = True
1644 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1645 thread = Thread( target = self.update_feed_cache_proc, args = args)
1646 thread.start()
1648 def treeview_channel_set_color( self, channel, color ):
1649 if self.treeChannels.get_model():
1650 if color in self.channel_colors:
1651 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1652 else:
1653 self.treeChannels.get_model().set(channel.iter, 8, color)
1655 def on_gPodder_delete_event(self, widget, *args):
1656 """Called when the GUI wants to close the window
1657 Displays a confirmation dialog (and closes/hides gPodder)
1660 downloading = services.download_status_manager.has_items()
1662 # Only iconify if we are using the window's "X" button,
1663 # but not when we are using "Quit" in the menu or toolbar
1664 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1665 self.iconify_main_window()
1666 elif gl.config.on_quit_ask or downloading:
1667 if gpodder.interface == gpodder.MAEMO:
1668 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1669 if result:
1670 self.close_gpodder()
1671 else:
1672 return True
1673 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1674 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1675 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1677 title = _('Quit gPodder')
1678 if downloading:
1679 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1680 else:
1681 message = _('Do you really want to quit gPodder now?')
1683 dialog.set_title(title)
1684 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1685 if not downloading:
1686 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1687 dialog.vbox.pack_start(cb_ask)
1688 cb_ask.show_all()
1690 result = dialog.run()
1691 dialog.destroy()
1693 if result == gtk.RESPONSE_CLOSE:
1694 if not downloading and cb_ask.get_active() == True:
1695 gl.config.on_quit_ask = False
1696 self.close_gpodder()
1697 else:
1698 self.close_gpodder()
1700 return True
1702 def close_gpodder(self):
1703 """ clean everything and exit properly
1705 if self.channels:
1706 if save_channels(self.channels):
1707 if gl.config.my_gpodder_autoupload:
1708 log('Uploading to my.gpodder.org on close', sender=self)
1709 util.idle_add(self.on_upload_to_mygpo, None)
1710 else:
1711 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1713 services.download_status_manager.cancel_all()
1714 self.gPodder.hide()
1715 while gtk.events_pending():
1716 gtk.main_iteration(False)
1718 db.close()
1720 self.gtk_main_quit()
1721 sys.exit( 0)
1723 def get_old_episodes(self):
1724 episodes = []
1725 for channel in self.channels:
1726 for episode in channel.get_downloaded_episodes():
1727 if episode.is_old() and not episode.is_locked and episode.is_played:
1728 episodes.append(episode)
1729 return episodes
1731 def for_each_selected_episode_url( self, callback):
1732 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1733 for path in paths:
1734 url = model.get_value( model.get_iter( path), 0)
1735 try:
1736 callback( url)
1737 except Exception, e:
1738 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1740 self.update_selected_episode_list_icons()
1741 self.updateComboBox(only_selected_channel=True)
1743 def delete_episode_list( self, episodes, confirm = True):
1744 if len(episodes) == 0:
1745 return
1747 if len(episodes) == 1:
1748 message = _('Do you really want to delete this episode?')
1749 else:
1750 message = _('Do you really want to delete %d episodes?') % len(episodes)
1752 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1753 return
1755 for episode in episodes:
1756 log('Deleting episode: %s', episode.title, sender = self)
1757 episode.delete_from_disk()
1759 self.download_status_updated()
1761 def on_itemRemoveOldEpisodes_activate( self, widget):
1762 columns = (
1763 ('title_and_description', None, None, _('Episode')),
1764 ('channel_prop', None, None, _('Podcast')),
1765 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1766 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1767 ('played_prop', None, None, _('Status')),
1768 ('age_prop', None, None, _('Downloaded')),
1771 selection_buttons = {
1772 _('Select played'): lambda episode: episode.is_played,
1773 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1776 instructions = _('Select the episodes you want to delete from your hard disk.')
1778 episodes = []
1779 selected = []
1780 for channel in self.channels:
1781 for episode in channel.get_downloaded_episodes():
1782 if not episode.is_locked:
1783 episodes.append(episode)
1784 selected.append(episode.is_played)
1786 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1787 episodes = episodes, selected = selected, columns = columns, \
1788 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1789 selection_buttons = selection_buttons)
1791 def mark_selected_episodes_new(self):
1792 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1793 self.for_each_selected_episode_url(callback)
1795 def mark_selected_episodes_old(self):
1796 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1797 self.for_each_selected_episode_url(callback)
1799 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1800 if toggle:
1801 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1802 else:
1803 callback = lambda url: db.mark_episode(url, is_played=new_value)
1805 self.for_each_selected_episode_url(callback)
1807 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1808 if toggle:
1809 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1810 else:
1811 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1813 self.for_each_selected_episode_url(callback)
1815 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1816 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
1817 db.update_channel_lock(self.active_channel)
1819 if self.active_channel.channel_is_locked:
1820 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
1821 else:
1822 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
1824 for episode in self.active_channel.get_all_episodes():
1825 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
1827 self.updateComboBox(only_selected_channel=True)
1829 def on_item_email_subscriptions_activate(self, widget):
1830 if not self.channels:
1831 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1832 elif not gl.send_subscriptions():
1833 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1835 def on_itemUpdateChannel_activate(self, widget=None):
1836 self.update_feed_cache(channels=[self.active_channel,])
1838 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1839 restore_from = can_restore_from_opml()
1841 if self.channels:
1842 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1843 elif restore_from is not None:
1844 title = _('Database upgrade required')
1845 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?')
1846 if self.show_confirmation(message, title):
1847 add_callback = lambda url: self.add_new_channel(url, False, True)
1848 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1849 w.set_has_separator(False)
1850 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1851 w.set_default_size(500, -1)
1852 pb = gtk.ProgressBar()
1853 l = gtk.Label()
1854 l.set_padding(6, 3)
1855 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1856 l.set_alignment(0.0, 0.5)
1857 w.vbox.pack_start(l)
1858 l = gtk.Label()
1859 l.set_padding(6, 3)
1860 l.set_alignment(0.0, 0.5)
1861 l.set_text(_('Please wait while your settings are converted.'))
1862 w.vbox.pack_start(l)
1863 w.vbox.pack_start(pb)
1864 lb = gtk.Label()
1865 lb.set_ellipsize(pango.ELLIPSIZE_END)
1866 lb.set_alignment(0.0, 0.5)
1867 lb.set_padding(6, 6)
1868 w.vbox.pack_start(lb)
1870 def set_pb_status(pb, lb, fraction, text):
1871 pb.set_fraction(float(fraction)/100.0)
1872 pb.set_text('%.0f %%' % fraction)
1873 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1874 while gtk.events_pending():
1875 gtk.main_iteration(False)
1876 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1877 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1878 w.show_all()
1879 start = datetime.datetime.now()
1880 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1881 # Refresh the view with the updated episodes
1882 self.updateComboBox()
1883 time_taken = str(datetime.datetime.now()-start)
1884 status_callback(100.0, _('Migration finished in %s') % time_taken)
1885 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1886 w.run()
1887 w.destroy()
1888 else:
1889 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
1891 def download_episode_list(self, episodes):
1892 services.download_status_manager.start_batch_mode()
1893 for episode in episodes:
1894 log('Downloading episode: %s', episode.title, sender = self)
1895 filename = episode.local_filename()
1896 if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
1897 download.DownloadThread(episode.channel, episode, self.notification).start()
1898 services.download_status_manager.end_batch_mode()
1900 def new_episodes_show(self, episodes):
1901 columns = (
1902 ('title_and_description', None, None, _('Episode')),
1903 ('channel_prop', None, None, _('Podcast')),
1904 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1905 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1908 if len(episodes) > 0:
1909 instructions = _('Select the episodes you want to download now.')
1911 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1912 episodes=episodes, columns=columns, selected_default=True, \
1913 stock_ok_button = 'gpodder-download', \
1914 callback=self.download_episode_list, \
1915 remove_callback=lambda e: e.mark_old(), \
1916 remove_action=_('Never download'), \
1917 remove_finished=self.episode_new_status_changed)
1918 else:
1919 title = _('No new episodes')
1920 message = _('No new episodes to download.\nPlease check for new episodes later.')
1921 self.show_message(message, title)
1923 def on_itemDownloadAllNew_activate(self, widget, *args):
1924 self.download_all_new()
1926 def download_all_new(self, channels=None):
1927 if channels is None:
1928 channels = self.channels
1929 episodes = []
1930 for channel in channels:
1931 for episode in channel.get_new_episodes():
1932 episodes.append(episode)
1933 self.new_episodes_show(episodes)
1935 def get_all_episodes(self, exclude_nonsignificant=True ):
1936 """'exclude_nonsignificant' will exclude non-downloaded episodes
1937 and all episodes from channels that are set to skip when syncing"""
1938 episode_list = []
1939 for channel in self.channels:
1940 if not channel.sync_to_devices and exclude_nonsignificant:
1941 log('Skipping channel: %s', channel.title, sender=self)
1942 continue
1943 for episode in channel.get_all_episodes():
1944 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1945 episode_list.append(episode)
1946 return episode_list
1948 def ipod_delete_played(self, device):
1949 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1950 episodes_on_device = device.get_all_tracks()
1951 for local_episode in all_episodes:
1952 device_episode = device.episode_on_device(local_episode)
1953 if device_episode and ( local_episode.is_played and not local_episode.is_locked
1954 or local_episode.state == db.STATE_DELETED ):
1955 log("mp3_player_delete_played: removing %s" % device_episode.title)
1956 device.remove_track(device_episode)
1958 def on_sync_to_ipod_activate(self, widget, episodes=None):
1959 # make sure gpod is available before even trying to sync
1960 if gl.config.device_type == 'ipod' and not sync.gpod_available:
1961 title = _('Cannot Sync To iPod')
1962 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
1963 self.notification( message, title )
1964 return
1965 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
1966 title = _('Cannot sync to MTP device')
1967 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
1968 self.notification( message, title )
1969 return
1971 device = sync.open_device()
1972 device.register( 'post-done', self.sync_to_ipod_completed )
1974 if device is None:
1975 title = _('No device configured')
1976 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1977 self.notification(message, title)
1978 return
1980 if not device.open():
1981 title = _('Cannot open device')
1982 message = _('There has been an error opening your device.')
1983 self.notification(message, title)
1984 return
1986 if gl.config.ipod_purge_old_episodes:
1987 device.purge()
1989 sync_all_episodes = not bool(episodes)
1991 if episodes is None:
1992 episodes = self.get_all_episodes()
1994 # make sure we have enough space on the device
1995 total_size = 0
1996 free_space = device.get_free_space()
1997 for episode in episodes:
1998 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
1999 total_size += util.calculate_size(str(episode.local_filename()))
2001 if total_size > free_space:
2002 # can be negative because of the 10 MiB for reserved for the iTunesDB
2003 free_space = max( free_space, 0 )
2004 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
2005 title = _('Not enough space left on device.')
2006 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
2007 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
2008 self.notification(message, title)
2009 else:
2010 # start syncing!
2011 gPodderSync(device=device, gPodder=self)
2012 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2013 if self.tray_icon:
2014 self.tray_icon.set_synchronisation_device(device)
2016 def sync_to_ipod_completed(self, device, successful_sync):
2017 device.unregister( 'post-done', self.sync_to_ipod_completed )
2019 if self.tray_icon:
2020 self.tray_icon.release_synchronisation_device()
2022 if not successful_sync:
2023 title = _('Error closing device')
2024 message = _('There has been an error closing your device.')
2025 self.notification(message, title)
2027 # update model for played state updates after sync
2028 util.idle_add(self.updateComboBox)
2030 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2031 if sync_all_episodes:
2032 device.add_tracks(episodes)
2033 # 'only_sync_not_played' must be used or else all the played
2034 # tracks will be copied then immediately deleted
2035 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2036 self.ipod_delete_played(device)
2037 else:
2038 device.add_tracks(episodes, force_played=True)
2039 device.close()
2041 def ipod_cleanup_callback(self, device, tracks):
2042 title = _('Delete podcasts from device?')
2043 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?')
2044 if len(tracks) > 0 and self.show_confirmation(message, title):
2045 device.remove_tracks(tracks)
2047 if not device.close():
2048 title = _('Error closing device')
2049 message = _('There has been an error closing your device.')
2050 self.show_message(message, title)
2051 return
2053 def on_cleanup_ipod_activate(self, widget, *args):
2054 columns = (
2055 ('title', None, None, _('Episode')),
2056 ('podcast', None, None, _('Podcast')),
2057 ('filesize', None, None, _('Size')),
2058 ('modified', None, None, _('Copied')),
2059 ('playcount', None, None, _('Play count')),
2060 ('released', None, None, _('Released')),
2063 device = sync.open_device()
2065 if device is None:
2066 title = _('No device configured')
2067 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2068 self.show_message(message, title)
2069 return
2071 if not device.open():
2072 title = _('Cannot open device')
2073 message = _('There has been an error opening your device.')
2074 self.show_message(message, title)
2075 return
2077 gPodderSync(device=device, gPodder=self)
2079 tracks = device.get_all_tracks()
2080 if len(tracks) > 0:
2081 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2082 wanted_columns = []
2083 for key, sort_name, sort_type, caption in columns:
2084 want_this_column = False
2085 for track in tracks:
2086 if getattr(track, key) is not None:
2087 want_this_column = True
2088 break
2090 if want_this_column:
2091 wanted_columns.append((key, sort_name, sort_type, caption))
2092 title = _('Remove podcasts from device')
2093 instructions = _('Select the podcast episodes you want to remove from your device.')
2094 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2095 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2096 else:
2097 title = _('No files on device')
2098 message = _('The devices contains no files to be removed.')
2099 self.show_message(message, title)
2100 device.close()
2102 def on_manage_device_playlist(self, widget):
2103 # make sure gpod is available before even trying to sync
2104 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2105 title = _('Cannot manage iPod playlist')
2106 message = _('This feature is not available for iPods.')
2107 self.notification( message, title )
2108 return
2109 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2110 title = _('Cannot manage MTP device playlist')
2111 message = _('This feature is not available for MTP devices.')
2112 self.notification( message, title )
2113 return
2115 device = sync.open_device()
2117 if device is None:
2118 title = _('No device configured')
2119 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2120 self.notification(message, title)
2121 return
2123 if not device.open():
2124 title = _('Cannot open device')
2125 message = _('There has been an error opening your device.')
2126 self.notification(message, title)
2127 return
2129 gPodderPlaylist(device=device, gPodder=self)
2130 device.close()
2132 def show_hide_tray_icon(self):
2133 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2134 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
2135 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2136 self.tray_icon.set_visible(False)
2137 del self.tray_icon
2138 self.tray_icon = None
2140 if gl.config.minimize_to_tray and self.tray_icon:
2141 self.tray_icon.set_visible(self.minimized)
2142 elif self.tray_icon:
2143 self.tray_icon.set_visible(True)
2145 def on_itemShowToolbar_activate(self, widget):
2146 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2148 def on_itemShowDescription_activate(self, widget):
2149 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2151 def update_item_device( self):
2152 if gl.config.device_type != 'none':
2153 self.itemDevice.show_all()
2154 (label,) = self.itemDevice.get_children()
2155 label.set_text(gl.get_device_name())
2156 else:
2157 self.itemDevice.hide_all()
2159 def properties_closed( self):
2160 self.show_hide_tray_icon()
2161 self.update_item_device()
2162 self.updateComboBox()
2164 def on_itemPreferences_activate(self, widget, *args):
2165 if gpodder.interface == gpodder.GUI:
2166 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2167 else:
2168 gPodderMaemoPreferences()
2170 def on_itemDependencies_activate(self, widget):
2171 gPodderDependencyManager()
2173 def on_add_new_google_search(self, widget, *args):
2174 def add_google_video_search(query):
2175 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2177 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2179 def require_my_gpodder_authentication(self):
2180 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2181 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'))
2182 if success and authentication[0] and authentication[1]:
2183 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2184 return True
2185 else:
2186 return False
2188 return True
2190 def my_gpodder_offer_autoupload(self):
2191 if not gl.config.my_gpodder_autoupload:
2192 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')):
2193 gl.config.my_gpodder_autoupload = True
2195 def on_download_from_mygpo(self, widget):
2196 if self.require_my_gpodder_authentication():
2197 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2198 opml_data = client.download_subscriptions()
2199 if len(opml_data) > 0:
2200 fp = open(gl.channel_opml_file, 'w')
2201 fp.write(opml_data)
2202 fp.close()
2203 (added, skipped) = (0, 0)
2204 i = opml.Importer(gl.channel_opml_file)
2205 for item in i.items:
2206 url = item['url']
2207 if url not in (c.url for c in self.channels):
2208 self.add_new_channel(url, ask_download_new=False, block=True)
2209 added += 1
2210 else:
2211 log('Already added: %s', url, sender=self)
2212 skipped += 1
2213 self.updateComboBox()
2214 if added > 0:
2215 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2216 elif widget is not None:
2217 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2218 self.my_gpodder_offer_autoupload()
2219 else:
2220 gl.config.my_gpodder_password = ''
2221 self.on_download_from_mygpo(widget)
2222 else:
2223 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2225 def on_upload_to_mygpo(self, widget):
2226 if self.require_my_gpodder_authentication():
2227 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2228 save_channels(self.channels)
2229 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2230 if widget is not None:
2231 self.show_message('\n'.join(messages), _('Results of upload'))
2232 if not success:
2233 gl.config.my_gpodder_password = ''
2234 self.on_upload_to_mygpo(widget)
2235 else:
2236 self.my_gpodder_offer_autoupload()
2237 elif not success:
2238 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2239 elif widget is not None:
2240 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2242 def on_itemAddChannel_activate(self, widget, *args):
2243 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2245 def on_itemEditChannel_activate(self, widget, *args):
2246 if self.active_channel is None:
2247 title = _('No podcast selected')
2248 message = _('Please select a podcast in the podcasts list to edit.')
2249 self.show_message( message, title)
2250 return
2252 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2254 def change_channel_url(self, old_url, new_url):
2255 channel = None
2256 try:
2257 channel = podcastChannel.load(url=new_url, create=True)
2258 except:
2259 channel = None
2261 if channel is None:
2262 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2263 return
2265 for channel in self.channels:
2266 if channel.url == old_url:
2267 log('=> change channel url from %s to %s', old_url, new_url)
2268 old_save_dir = channel.save_dir
2269 channel.url = new_url
2270 new_save_dir = channel.save_dir
2271 log('old save dir=%s', old_save_dir, sender=self)
2272 log('new save dir=%s', new_save_dir, sender=self)
2273 files = glob.glob(os.path.join(old_save_dir, '*'))
2274 log('moving %d files to %s', len(files), new_save_dir, sender=self)
2275 for file in files:
2276 log('moving %s', file, sender=self)
2277 shutil.move(file, new_save_dir)
2278 try:
2279 os.rmdir(old_save_dir)
2280 except:
2281 log('Warning: cannot delete %s', old_save_dir, sender=self)
2283 save_channels(self.channels)
2284 # update feed cache and select the podcast with the new URL afterwards
2285 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2287 def on_itemRemoveChannel_activate(self, widget, *args):
2288 try:
2289 if gpodder.interface == gpodder.GUI:
2290 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2291 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2292 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2294 title = _('Remove podcast and episodes?')
2295 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2297 dialog.set_title(title)
2298 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2300 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2301 dialog.vbox.pack_start(cb_ask)
2302 cb_ask.show_all()
2303 affirmative = gtk.RESPONSE_YES
2304 elif gpodder.interface == gpodder.MAEMO:
2305 cb_ask = gtk.CheckButton('') # dummy check button
2306 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2307 affirmative = gtk.RESPONSE_OK
2309 result = dialog.run()
2310 dialog.destroy()
2312 if result == affirmative:
2313 # delete downloaded episodes only if checkbox is unchecked
2314 if cb_ask.get_active() == False:
2315 self.active_channel.remove_downloaded()
2316 else:
2317 log('Not removing downloaded episodes', sender=self)
2319 # only delete partial files if we do not have any downloads in progress
2320 delete_partial = not services.download_status_manager.has_items()
2321 gl.clean_up_downloads(delete_partial)
2323 # cancel any active downloads from this channel
2324 if not delete_partial:
2325 for episode in self.active_channel.get_all_episodes():
2326 services.download_status_manager.cancel_by_url(episode.url)
2328 # get the URL of the podcast we want to select next
2329 position = self.channels.index(self.active_channel)
2330 if position == len(self.channels)-1:
2331 # this is the last podcast, so select the URL
2332 # of the item before this one (i.e. the "new last")
2333 select_url = self.channels[position-1].url
2334 else:
2335 # there is a podcast after the deleted one, so
2336 # we simply select the one that comes after it
2337 select_url = self.channels[position+1].url
2339 # Remove the channel
2340 self.active_channel.delete()
2341 self.channels.remove(self.active_channel)
2342 self.channel_list_changed = True
2343 save_channels(self.channels)
2345 # Re-load the channels and select the desired new channel
2346 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2347 except:
2348 log('There has been an error removing the channel.', traceback=True, sender=self)
2349 self.update_podcasts_tab()
2351 def get_opml_filter(self):
2352 filter = gtk.FileFilter()
2353 filter.add_pattern('*.opml')
2354 filter.add_pattern('*.xml')
2355 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2356 return filter
2358 def on_item_import_from_file_activate(self, widget, filename=None):
2359 if filename is None:
2360 if gpodder.interface == gpodder.GUI:
2361 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2362 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2363 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2364 elif gpodder.interface == gpodder.MAEMO:
2365 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2366 dlg.set_filter(self.get_opml_filter())
2367 response = dlg.run()
2368 filename = None
2369 if response == gtk.RESPONSE_OK:
2370 filename = dlg.get_filename()
2371 dlg.destroy()
2373 if filename is not None:
2374 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))
2376 def on_itemExportChannels_activate(self, widget, *args):
2377 if not self.channels:
2378 title = _('Nothing to export')
2379 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2380 self.show_message( message, title)
2381 return
2383 if gpodder.interface == gpodder.GUI:
2384 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2385 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2386 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2387 elif gpodder.interface == gpodder.MAEMO:
2388 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2389 dlg.set_filter(self.get_opml_filter())
2390 response = dlg.run()
2391 if response == gtk.RESPONSE_OK:
2392 filename = dlg.get_filename()
2393 dlg.destroy()
2394 exporter = opml.Exporter( filename)
2395 if exporter.write(self.channels):
2396 if len(self.channels) == 1:
2397 title = _('One subscription exported')
2398 else:
2399 title = _('%d subscriptions exported') % len(self.channels)
2400 self.show_message(_('Your podcast list has been successfully exported.'), title)
2401 else:
2402 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2403 else:
2404 dlg.destroy()
2406 def on_itemImportChannels_activate(self, widget, *args):
2407 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))
2409 def on_homepage_activate(self, widget, *args):
2410 util.open_website(app_website)
2412 def on_wiki_activate(self, widget, *args):
2413 util.open_website('http://wiki.gpodder.org/')
2415 def on_bug_tracker_activate(self, widget, *args):
2416 util.open_website('http://bugs.gpodder.org/')
2418 def on_itemAbout_activate(self, widget, *args):
2419 dlg = gtk.AboutDialog()
2420 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2421 dlg.set_version( app_version)
2422 dlg.set_copyright( app_copyright)
2423 dlg.set_website( app_website)
2424 dlg.set_translator_credits( _('translator-credits'))
2425 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2427 if gpodder.interface == gpodder.GUI:
2428 # For the "GUI" version, we add some more
2429 # items to the about dialog (credits and logo)
2430 dlg.set_authors(app_authors)
2431 try:
2432 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2433 except:
2434 pass
2436 dlg.run()
2438 def on_wNotebook_switch_page(self, widget, *args):
2439 page_num = args[1]
2440 if gpodder.interface == gpodder.MAEMO:
2441 page = self.wNotebook.get_nth_page(page_num)
2442 tab_label = self.wNotebook.get_tab_label(page).get_text()
2443 if page_num == 0 and self.active_channel is not None:
2444 self.set_title(self.active_channel.title)
2445 else:
2446 self.set_title(tab_label)
2447 if page_num == 0:
2448 self.play_or_download()
2449 else:
2450 self.toolDownload.set_sensitive( False)
2451 self.toolPlay.set_sensitive( False)
2452 self.toolTransfer.set_sensitive( False)
2453 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2455 def on_treeChannels_row_activated(self, widget, *args):
2456 self.on_itemEditChannel_activate( self.treeChannels)
2458 def on_treeChannels_cursor_changed(self, widget, *args):
2459 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2461 if model is not None and iter is not None:
2462 old_active_channel = self.active_channel
2463 (id,) = model.get_path(iter)
2464 self.active_channel = self.channels[id]
2466 if self.active_channel == old_active_channel:
2467 return
2469 if gpodder.interface == gpodder.MAEMO:
2470 self.set_title(self.active_channel.title)
2471 self.itemEditChannel.show_all()
2472 self.itemRemoveChannel.show_all()
2473 self.channel_toggle_lock.show_all()
2474 if self.active_channel.channel_is_locked:
2475 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2476 else:
2477 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2479 else:
2480 self.active_channel = None
2481 self.itemEditChannel.hide_all()
2482 self.itemRemoveChannel.hide_all()
2483 self.channel_toggle_lock.hide_all()
2485 self.updateTreeView()
2487 def on_entryAddChannel_changed(self, widget, *args):
2488 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2489 self.btnAddChannel.set_sensitive( active)
2491 def on_btnAddChannel_clicked(self, widget, *args):
2492 url = self.entryAddChannel.get_text()
2493 self.entryAddChannel.set_text('')
2494 self.add_new_channel( url)
2496 def on_btnEditChannel_clicked(self, widget, *args):
2497 self.on_itemEditChannel_activate( widget, args)
2499 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2501 What this function does depends on from which widget it is called.
2502 It gets the selected episodes of the current podcast and runs one
2503 of the following actions on them:
2505 * Transfer (to MP3 player, iPod, etc..)
2506 * Playback/open files
2507 * Show the episode info dialog
2508 * Download episodes
2510 try:
2511 selection = self.treeAvailable.get_selection()
2512 (model, paths) = selection.get_selected_rows()
2514 wname = widget.get_name()
2515 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2516 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2517 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2519 episodes = []
2520 for path in paths:
2521 it = model.get_iter(path)
2522 url = model.get_value(it, 0)
2523 episode = self.active_channel.find_episode(url)
2524 episodes.append(episode)
2526 if len(episodes) == 0:
2527 log('No episodes selected', sender=self)
2529 if do_transfer:
2530 self.on_sync_to_ipod_activate(widget, episodes)
2531 elif do_playback:
2532 for episode in episodes:
2533 # Make sure to mark the episode as downloaded
2534 if os.path.exists(episode.local_filename()):
2535 episode.channel.addDownloadedItem(episode)
2536 self.playback_episode(episode)
2537 elif gl.config.enable_streaming:
2538 self.playback_episode(episode, stream=True)
2539 elif do_epdialog:
2540 play_callback = lambda: self.playback_episode(episode)
2541 def download_callback():
2542 self.download_episode_list([episode])
2543 self.play_or_download()
2544 gPodderEpisode(episode=episode, download_callback=download_callback, play_callback=play_callback)
2545 else:
2546 self.download_episode_list(episodes)
2547 self.play_or_download()
2548 except:
2549 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2551 def on_treeAvailable_button_release_event(self, widget, *args):
2552 self.play_or_download()
2554 def auto_update_procedure(self, first_run=False):
2555 log('auto_update_procedure() got called', sender=self)
2556 if not first_run and gl.config.auto_update_feeds and self.minimized:
2557 self.update_feed_cache(force_update=True)
2559 next_update = 60*1000*gl.config.auto_update_frequency
2560 gobject.timeout_add(next_update, self.auto_update_procedure)
2562 def on_treeDownloads_row_activated(self, widget, *args):
2563 cancel_urls = []
2565 if self.wNotebook.get_current_page() > 0:
2566 # Use the download list treeview + model
2567 ( tree, column ) = ( self.treeDownloads, 3 )
2568 else:
2569 # Use the available podcasts treeview + model
2570 ( tree, column ) = ( self.treeAvailable, 0 )
2572 selection = tree.get_selection()
2573 (model, paths) = selection.get_selected_rows()
2574 for path in paths:
2575 url = model.get_value( model.get_iter( path), column)
2576 cancel_urls.append( url)
2578 if len( cancel_urls) == 0:
2579 log('Nothing selected.', sender = self)
2580 return
2582 if len( cancel_urls) == 1:
2583 title = _('Cancel download?')
2584 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2585 else:
2586 title = _('Cancel downloads?')
2587 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2589 if self.show_confirmation( message, title):
2590 services.download_status_manager.start_batch_mode()
2591 for url in cancel_urls:
2592 services.download_status_manager.cancel_by_url( url)
2593 services.download_status_manager.end_batch_mode()
2594 self.play_or_download()
2596 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2597 self.on_treeDownloads_row_activated( widget, None)
2599 def on_btnCancelAll_clicked(self, widget, *args):
2600 self.treeDownloads.get_selection().select_all()
2601 self.on_treeDownloads_row_activated( self.toolCancel, None)
2602 self.treeDownloads.get_selection().unselect_all()
2604 def on_btnDownloadedDelete_clicked(self, widget, *args):
2605 if self.active_channel is None:
2606 return
2608 channel_url = self.active_channel.url
2609 selection = self.treeAvailable.get_selection()
2610 ( model, paths ) = selection.get_selected_rows()
2612 if selection.count_selected_rows() == 0:
2613 log( 'Nothing selected - will not remove any downloaded episode.')
2614 return
2616 if selection.count_selected_rows() == 1:
2617 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2619 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2620 if episode['is_locked']:
2621 title = _('%s is locked') % episode_title
2622 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2623 self.notification(message, title)
2624 return
2626 title = _('Remove %s?') % episode_title
2627 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.")
2628 else:
2629 title = _('Remove %d episodes?') % selection.count_selected_rows()
2630 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.')
2632 locked_count = 0
2633 for path in paths:
2634 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2635 if episode['is_locked']:
2636 locked_count += 1
2638 if selection.count_selected_rows() == locked_count:
2639 title = _('Episodes are locked')
2640 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2641 self.notification(message, title)
2642 return
2643 elif locked_count > 0:
2644 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2645 message = _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
2647 # if user confirms deletion, let's remove some stuff ;)
2648 if self.show_confirmation( message, title):
2649 try:
2650 # iterate over the selection, see also on_treeDownloads_row_activated
2651 for path in paths:
2652 url = model.get_value( model.get_iter( path), 0)
2653 self.active_channel.delete_episode_by_url( url)
2655 # now, clear local db cache so we can re-read it
2656 self.updateComboBox()
2657 except:
2658 log( 'Error while deleting (some) downloads.')
2660 # only delete partial files if we do not have any downloads in progress
2661 delete_partial = not services.download_status_manager.has_items()
2662 gl.clean_up_downloads(delete_partial)
2663 self.updateTreeView()
2665 def on_key_press(self, widget, event):
2666 # Allow tab switching with Ctrl + PgUp/PgDown
2667 if event.state & gtk.gdk.CONTROL_MASK:
2668 if event.keyval == gtk.keysyms.Page_Up:
2669 self.wNotebook.prev_page()
2670 return True
2671 elif event.keyval == gtk.keysyms.Page_Down:
2672 self.wNotebook.next_page()
2673 return True
2675 # After this code we only handle Maemo hardware keys,
2676 # so if we are not a Maemo app, we don't do anything
2677 if gpodder.interface != gpodder.MAEMO:
2678 return False
2680 if event.keyval == gtk.keysyms.F6:
2681 if self.fullscreen:
2682 self.window.unfullscreen()
2683 else:
2684 self.window.fullscreen()
2685 if event.keyval == gtk.keysyms.Escape:
2686 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2687 self.vboxChannelNavigator.set_property('visible', new_visibility)
2688 self.column_size.set_visible(not new_visibility)
2689 self.column_released.set_visible(not new_visibility)
2691 diff = 0
2692 if event.keyval == gtk.keysyms.F7: #plus
2693 diff = 1
2694 elif event.keyval == gtk.keysyms.F8: #minus
2695 diff = -1
2697 if diff != 0:
2698 selection = self.treeChannels.get_selection()
2699 (model, iter) = selection.get_selected()
2700 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2701 self.on_treeChannels_cursor_changed(self.treeChannels)
2703 def window_state_event(self, widget, event):
2704 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2705 self.fullscreen = True
2706 else:
2707 self.fullscreen = False
2709 old_minimized = self.minimized
2711 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2712 if gpodder.interface == gpodder.MAEMO:
2713 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2715 if old_minimized != self.minimized and self.tray_icon:
2716 self.gPodder.set_skip_taskbar_hint(self.minimized)
2717 elif not self.tray_icon:
2718 self.gPodder.set_skip_taskbar_hint(False)
2720 if gl.config.minimize_to_tray and self.tray_icon:
2721 self.tray_icon.set_visible(self.minimized)
2723 def uniconify_main_window(self):
2724 if self.minimized:
2725 self.gPodder.present()
2727 def iconify_main_window(self):
2728 if not self.minimized:
2729 self.gPodder.iconify()
2731 def update_podcasts_tab(self):
2732 if len(self.channels):
2733 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2734 else:
2735 self.label2.set_text(_('Podcasts'))
2737 class gPodderChannel(GladeWidget):
2738 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description']
2740 def new(self):
2741 global WEB_BROWSER_ICON
2742 self.changed = False
2743 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2744 self.gPodderChannel.set_title( self.channel.title)
2745 self.entryTitle.set_text( self.channel.title)
2746 self.entryURL.set_text( self.channel.url)
2748 self.LabelDownloadTo.set_text( self.channel.save_dir)
2749 self.LabelWebsite.set_text( self.channel.link)
2751 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2752 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2753 if self.channel.username:
2754 self.FeedUsername.set_text( self.channel.username)
2755 if self.channel.password:
2756 self.FeedPassword.set_text( self.channel.password)
2758 services.cover_downloader.register('cover-available', self.cover_download_finished)
2759 services.cover_downloader.request_cover(self.channel)
2761 # Hide the website button if we don't have a valid URL
2762 if not self.channel.link:
2763 self.btn_website.hide_all()
2765 b = gtk.TextBuffer()
2766 b.set_text( self.channel.description)
2767 self.channel_description.set_buffer( b)
2769 #Add Drag and Drop Support
2770 flags = gtk.DEST_DEFAULT_ALL
2771 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2772 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2773 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2774 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2776 def on_btn_website_clicked(self, widget):
2777 util.open_website(self.channel.link)
2779 def on_btnDownloadCover_clicked(self, widget):
2780 if gpodder.interface == gpodder.GUI:
2781 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2782 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2783 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2784 elif gpodder.interface == gpodder.MAEMO:
2785 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2787 if dlg.run() == gtk.RESPONSE_OK:
2788 url = dlg.get_uri()
2789 services.cover_downloader.replace_cover(self.channel, url)
2791 dlg.destroy()
2793 def on_btnClearCover_clicked(self, widget):
2794 services.cover_downloader.replace_cover(self.channel)
2796 def cover_download_finished(self, channel_url, pixbuf):
2797 if pixbuf is not None:
2798 self.imgCover.set_from_pixbuf(pixbuf)
2799 self.gPodderChannel.show()
2801 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2802 files = sel.data.strip().split('\n')
2803 if len(files) != 1:
2804 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2805 return
2807 file = files[0]
2809 if file.startswith('file://') or file.startswith('http://'):
2810 services.cover_downloader.replace_cover(self.channel, file)
2811 return
2813 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2815 def on_gPodderChannel_destroy(self, widget, *args):
2816 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2818 def on_btnOK_clicked(self, widget, *args):
2819 entered_url = self.entryURL.get_text()
2820 channel_url = self.channel.url
2822 if entered_url != channel_url:
2823 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2824 if hasattr(self, 'callback_change_url'):
2825 self.gPodderChannel.hide_all()
2826 self.callback_change_url(channel_url, entered_url)
2828 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2829 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2830 self.channel.set_custom_title( self.entryTitle.get_text())
2831 self.channel.username = self.FeedUsername.get_text().strip()
2832 self.channel.password = self.FeedPassword.get_text()
2833 self.channel.save()
2835 self.gPodderChannel.destroy()
2836 self.callback_closed()
2838 class gPodderAddPodcastDialog(GladeWidget):
2839 finger_friendly_widgets = ['btn_close', 'btn_add']
2841 def new(self):
2842 if not hasattr(self, 'url_callback'):
2843 log('No url callback set', sender=self)
2844 self.url_callback = None
2845 if hasattr(self, 'custom_label'):
2846 self.label_add.set_text(self.custom_label)
2847 if hasattr(self, 'custom_title'):
2848 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2849 if gpodder.interface == gpodder.MAEMO:
2850 self.entry_url.set_text('http://')
2852 def on_btn_close_clicked(self, widget):
2853 self.gPodderAddPodcastDialog.destroy()
2855 def on_entry_url_changed(self, widget):
2856 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2858 def on_btn_add_clicked(self, widget):
2859 url = self.entry_url.get_text()
2860 self.on_btn_close_clicked(widget)
2861 if self.url_callback is not None:
2862 self.url_callback(url)
2865 class gPodderMaemoPreferences(GladeWidget):
2866 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2868 def new(self):
2869 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2870 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2871 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2872 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2874 self.restart_required = False
2875 self.show_tray_icon.connect('clicked', self.on_restart_required)
2876 self.show_notifications.connect('clicked', self.on_restart_required)
2878 def on_restart_required(self, widget):
2879 self.restart_required = True
2881 def on_btn_advanced_clicked(self, widget):
2882 self.gPodderMaemoPreferences.destroy()
2883 gPodderConfigEditor()
2885 def on_btn_close_clicked(self, widget):
2886 self.gPodderMaemoPreferences.destroy()
2887 if self.restart_required:
2888 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2891 class gPodderProperties(GladeWidget):
2892 def new(self):
2893 if not hasattr( self, 'callback_finished'):
2894 self.callback_finished = None
2896 if gpodder.interface == gpodder.MAEMO:
2897 self.table5.hide_all() # player
2898 self.gPodderProperties.fullscreen()
2900 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2901 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2902 gl.config.connect_gtk_editable( 'player', self.openApp)
2903 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2904 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2905 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2906 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2907 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2908 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2909 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2910 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2911 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2912 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2913 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2914 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2915 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2916 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2917 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2918 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2919 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2920 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2921 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2922 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2924 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2925 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2927 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2929 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2930 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2931 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2933 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2934 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2936 if tagging_supported():
2937 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
2938 else:
2939 self.updatetags.set_sensitive( False)
2940 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2941 self.updatetags.set_label( new_label)
2943 # device type
2944 self.comboboxDeviceType.set_active( 0)
2945 if gl.config.device_type == 'ipod':
2946 self.comboboxDeviceType.set_active( 1)
2947 elif gl.config.device_type == 'filesystem':
2948 self.comboboxDeviceType.set_active( 2)
2949 elif gl.config.device_type == 'mtp':
2950 self.comboboxDeviceType.set_active( 3)
2952 # setup cell renderers
2953 cellrenderer = gtk.CellRendererPixbuf()
2954 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2955 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2956 cellrenderer = gtk.CellRendererText()
2957 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2958 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2960 cellrenderer = gtk.CellRendererPixbuf()
2961 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2962 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2963 cellrenderer = gtk.CellRendererText()
2964 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2965 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2967 if not hasattr(self, 'user_apps_reader'):
2968 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2970 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
2971 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
2973 if gpodder.interface == gpodder.GUI:
2974 self.user_apps_reader.read()
2976 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2977 index = self.find_active_audio_app()
2978 self.comboAudioPlayerApp.set_active(index)
2979 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2980 index = self.find_active_video_app()
2981 self.comboVideoPlayerApp.set_active(index)
2983 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2985 def is_row_separator(self, model, iter):
2986 return model.get_value(iter, 0) == ''
2988 def update_mountpoint( self, ipod):
2989 if ipod is None or ipod.mount_point is None:
2990 self.iPodMountpoint.set_label( '')
2991 else:
2992 self.iPodMountpoint.set_label( ipod.mount_point)
2994 def find_active_audio_app(self):
2995 model = self.comboAudioPlayerApp.get_model()
2996 iter = model.get_iter_first()
2997 index = 0
2998 while iter is not None:
2999 command = model.get_value(iter, 1)
3000 if command == self.openApp.get_text():
3001 return index
3002 iter = model.iter_next(iter)
3003 index += 1
3004 # return last item = custom command
3005 return index-1
3007 def find_active_video_app( self):
3008 model = self.comboVideoPlayerApp.get_model()
3009 iter = model.get_iter_first()
3010 index = 0
3011 while iter is not None:
3012 command = model.get_value(iter, 1)
3013 if command == self.openVideoApp.get_text():
3014 return index
3015 iter = model.iter_next(iter)
3016 index += 1
3017 # return last item = custom command
3018 return index-1
3020 def set_download_dir( self, new_download_dir, event = None):
3021 gl.downloaddir = self.chooserDownloadTo.get_filename()
3022 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3023 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'))
3025 if event:
3026 event.set()
3028 def on_auto_update_feeds_toggled( self, widget, *args):
3029 self.auto_update_frequency.set_sensitive(widget.get_active())
3031 def on_display_tray_icon_toggled( self, widget, *args):
3032 self.enable_notifications.set_sensitive(widget.get_active())
3033 self.minimize_to_tray.set_sensitive(widget.get_active())
3035 def on_cbCustomSyncName_toggled( self, widget, *args):
3036 self.entryCustomSyncName.set_sensitive( widget.get_active())
3038 def on_only_sync_not_played_toggled( self, widget, *args):
3039 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3040 if not widget.get_active():
3041 self.delete_episodes_marked_played.set_active(False)
3043 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3044 if widget.get_active() and self.only_sync_not_played.get_active():
3045 self.on_sync_leave.set_active(True)
3046 self.on_sync_delete.set_sensitive(not widget.get_active())
3047 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3049 def on_btnCustomSyncNameHelp_clicked( self, widget):
3050 examples = [
3051 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3052 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3053 '<i>{episode.published}</i> -&gt; <b>20070908</b>',
3054 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3057 info = [
3058 _('You can specify a custom format string for the file names on your MP3 player here.'),
3059 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3060 '\n'.join( [ ' %s' % s for s in examples ])
3063 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3065 def on_gPodderProperties_destroy(self, widget, *args):
3066 self.on_btnOK_clicked( widget, *args)
3068 def on_btnConfigEditor_clicked(self, widget, *args):
3069 self.on_btnOK_clicked(widget, *args)
3070 gPodderConfigEditor()
3072 def on_comboAudioPlayerApp_changed(self, widget, *args):
3073 # find out which one
3074 iter = self.comboAudioPlayerApp.get_active_iter()
3075 model = self.comboAudioPlayerApp.get_model()
3076 command = model.get_value( iter, 1)
3077 if command == '':
3078 if self.openApp.get_text() == 'default':
3079 self.openApp.set_text('')
3080 self.openApp.set_sensitive( True)
3081 self.openApp.show()
3082 self.labelCustomCommand.show()
3083 else:
3084 self.openApp.set_text( command)
3085 self.openApp.set_sensitive( False)
3086 self.openApp.hide()
3087 self.labelCustomCommand.hide()
3089 def on_comboVideoPlayerApp_changed(self, widget, *args):
3090 # find out which one
3091 iter = self.comboVideoPlayerApp.get_active_iter()
3092 model = self.comboVideoPlayerApp.get_model()
3093 command = model.get_value(iter, 1)
3094 if command == '':
3095 if self.openVideoApp.get_text() == 'default':
3096 self.openVideoApp.set_text('')
3097 self.openVideoApp.set_sensitive(True)
3098 self.openVideoApp.show()
3099 self.labelCustomVideoCommand.show()
3100 else:
3101 self.openVideoApp.set_text(command)
3102 self.openVideoApp.set_sensitive(False)
3103 self.openVideoApp.hide()
3104 self.labelCustomVideoCommand.hide()
3106 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3107 sens = not self.cbEnvironmentVariables.get_active()
3108 self.httpProxy.set_sensitive( sens)
3109 self.ftpProxy.set_sensitive( sens)
3111 def on_comboboxDeviceType_changed(self, widget, *args):
3112 active_item = self.comboboxDeviceType.get_active()
3114 # None
3115 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3116 self.imageSyncOptions, self. separatorSyncOptions,
3117 self.on_sync_mark_played, self.on_sync_delete,
3118 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
3119 for widget in sync_widgets:
3120 if active_item == 0:
3121 widget.hide_all()
3122 else:
3123 widget.show_all()
3125 # iPod
3126 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3127 self.ipod_write_gtkpod_extended)
3128 for widget in ipod_widgets:
3129 if active_item == 1:
3130 widget.show_all()
3131 else:
3132 widget.hide_all()
3134 # filesystem-based MP3 player
3135 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3136 self.cbChannelSubfolder, self.cbCustomSyncName,
3137 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
3138 for widget in fs_widgets:
3139 if active_item == 2:
3140 widget.show_all()
3141 else:
3142 widget.hide_all()
3144 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3145 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3146 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3147 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3148 fs.set_current_folder(self.iPodMountpoint.get_label())
3149 if fs.run() == gtk.RESPONSE_OK:
3150 self.iPodMountpoint.set_label( fs.get_filename())
3151 fs.destroy()
3153 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3154 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3155 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3156 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3157 fs.set_current_folder(self.filesystemMountpoint.get_label())
3158 if fs.run() == gtk.RESPONSE_OK:
3159 self.filesystemMountpoint.set_label( fs.get_filename())
3160 fs.destroy()
3162 def on_btnOK_clicked(self, widget, *args):
3163 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3164 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3166 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3167 new_download_dir = self.chooserDownloadTo.get_filename()
3168 download_dir_size = util.calculate_size( gl.downloaddir)
3169 download_dir_size_string = gl.format_filesize( download_dir_size)
3170 event = Event()
3172 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3173 dlg.vbox.set_spacing( 5)
3174 dlg.set_border_width( 5)
3176 label = gtk.Label()
3177 label.set_line_wrap( True)
3178 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3179 myprogressbar = gtk.ProgressBar()
3181 # put it all together
3182 dlg.vbox.pack_start( label)
3183 dlg.vbox.pack_end( myprogressbar)
3185 # switch windows
3186 dlg.show_all()
3187 self.gPodderProperties.hide_all()
3189 # hide action area and separator line
3190 dlg.action_area.hide()
3191 dlg.set_has_separator( False)
3193 args = ( new_download_dir, event, )
3195 thread = Thread( target = self.set_download_dir, args = args)
3196 thread.start()
3198 while not event.isSet():
3199 try:
3200 new_download_dir_size = util.calculate_size( new_download_dir)
3201 except:
3202 new_download_dir_size = 0
3203 if download_dir_size > 0:
3204 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3205 else:
3206 fract = 0.0
3207 if fract < 0.99:
3208 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3209 else:
3210 myprogressbar.set_text( _('Finishing... please wait.'))
3211 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3212 event.wait( 0.1)
3213 while gtk.events_pending():
3214 gtk.main_iteration( False)
3216 dlg.destroy()
3218 device_type = self.comboboxDeviceType.get_active()
3219 if device_type == 0:
3220 gl.config.device_type = 'none'
3221 elif device_type == 1:
3222 gl.config.device_type = 'ipod'
3223 elif device_type == 2:
3224 gl.config.device_type = 'filesystem'
3225 elif device_type == 3:
3226 gl.config.device_type = 'mtp'
3227 self.gPodderProperties.destroy()
3228 if self.callback_finished:
3229 self.callback_finished()
3232 class gPodderEpisode(GladeWidget):
3233 finger_friendly_widgets = ['episode_description', 'btnCloseWindow', 'btnDownload',
3234 'btnCancel', 'btnPlay', 'btn_website']
3236 def new(self):
3237 global WEB_BROWSER_ICON
3238 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
3239 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
3240 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
3242 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
3244 if gpodder.interface == gpodder.MAEMO:
3245 # Hide the advanced prefs expander
3246 self.expander1.hide_all()
3248 try:
3249 import gtkhtml2
3250 document = gtkhtml2.Document()
3251 document.connect('link-clicked', lambda d, url: util.open_website(url))
3252 def request_url(document, url, stream):
3253 stream.write(urllib2.urlopen(url).read())
3254 stream.close()
3255 document.connect('request-url', request_url)
3256 document.clear()
3257 document.open_stream('text/html')
3258 document.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3259 document.write_stream('<h2>%s</h2><small><em>from <a href="%s">%s</a>, released %s</em></small><br><hr style="border: 1px #eeeeee solid;">' % (saxutils.escape(self.episode.title), self.episode.link, saxutils.escape(self.episode.channel.title), self.episode.cute_pubdate()))
3260 document.write_stream(self.episode.description)
3261 document.write_stream('<br><hr style="border: 1px #eeeeee solid;"><p style="font-size: 8px;">%s</p>' % self.episode.link)
3262 document.write_stream('</body></html>')
3263 document.close_stream()
3265 self.episode_title.hide_all()
3266 self.channel_title.hide_all()
3267 self.btn_website.hide_all()
3268 self.expander1.hide_all()
3270 view = gtkhtml2.View()
3271 view.set_document(document)
3272 self.scrolledwindow4.remove(self.scrolledwindow4.get_child())
3273 self.scrolledwindow4.add(view)
3274 view.show()
3275 except ImportError, ie:
3276 b = gtk.TextBuffer()
3277 b.set_text(strip(util.remove_html_tags(self.episode.description)))
3278 self.episode_description.set_buffer( b)
3280 self.gPodderEpisode.set_title( self.episode.title)
3281 self.LabelDownloadLink.set_text( self.episode.url)
3282 self.LabelWebsiteLink.set_text( self.episode.link)
3283 self.labelPubDate.set_text(self.episode.cute_pubdate())
3285 # Hide the "Go to website" button if we don't have a valid URL
3286 if self.episode.link == self.episode.url or not self.episode.link:
3287 self.btn_website.hide_all()
3289 self.channel_title.set_markup(_('<i>from %s</i>') % saxutils.escape(self.episode.channel.title))
3291 self.hide_show_widgets()
3292 services.download_status_manager.request_progress_detail( self.episode.url)
3293 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3295 def on_btnCancel_clicked( self, widget):
3296 services.download_status_manager.cancel_by_url( self.episode.url)
3298 def on_gPodderEpisode_destroy( self, widget):
3299 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
3300 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
3302 def on_download_status_changed( self):
3303 self.hide_show_widgets()
3305 def on_btn_website_clicked(self, widget):
3306 util.open_website(self.episode.link)
3308 def on_download_status_progress( self, url, progress, speed):
3309 if url == self.episode.url:
3310 progress = float(min(100.0,max(0.0,progress)))
3311 self.progress_bar.set_fraction(progress/100.0)
3312 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
3314 def hide_show_widgets( self):
3315 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
3316 if is_downloading:
3317 self.progress_bar.show_all()
3318 self.btnCancel.show_all()
3319 self.btnPlay.hide_all()
3320 self.btnDownload.hide_all()
3321 else:
3322 self.progress_bar.hide_all()
3323 self.btnCancel.hide_all()
3324 if os.path.exists( self.episode.local_filename()):
3325 if self.episode.file_type() in ('audio', 'video'):
3326 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3327 else:
3328 self.btnPlay.set_label(gtk.STOCK_OPEN)
3329 self.btnPlay.set_use_stock(True)
3330 self.btnPlay.show_all()
3331 self.btnDownload.hide_all()
3332 else:
3333 self.btnPlay.hide_all()
3334 self.btnDownload.show_all()
3336 def on_btnCloseWindow_clicked(self, widget, *args):
3337 self.gPodderEpisode.destroy()
3339 def on_btnDownload_clicked(self, widget, *args):
3340 if self.download_callback:
3341 self.download_callback()
3343 def on_btnPlay_clicked(self, widget, *args):
3344 if self.play_callback:
3345 self.play_callback()
3347 self.gPodderEpisode.destroy()
3350 class gPodderSync(GladeWidget):
3351 def new(self):
3352 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3354 self.device.register('progress', self.on_progress)
3355 self.device.register('sub-progress', self.on_sub_progress)
3356 self.device.register('status', self.on_status)
3357 self.device.register('done', self.on_done)
3359 def on_progress(self, pos, max, text=None):
3360 if text is None:
3361 text = _('%d of %d done') % (pos, max)
3362 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3363 util.idle_add(self.progressbar.set_text, text)
3365 def on_sub_progress(self, percentage):
3366 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3368 def on_status(self, status):
3369 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3371 def on_done(self):
3372 util.idle_add(self.gPodderSync.destroy)
3373 if not self.gPodder.minimized:
3374 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3376 def on_gPodderSync_destroy(self, widget, *args):
3377 self.device.unregister('progress', self.on_progress)
3378 self.device.unregister('sub-progress', self.on_sub_progress)
3379 self.device.unregister('status', self.on_status)
3380 self.device.unregister('done', self.on_done)
3381 self.device.cancel()
3383 def on_cancel_button_clicked(self, widget, *args):
3384 self.device.cancel()
3387 class gPodderOpmlLister(GladeWidget):
3388 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3390 def new(self):
3391 # initiate channels list
3392 self.channels = []
3393 self.callback_for_channel = None
3394 self.callback_finished = None
3396 if hasattr(self, 'custom_title'):
3397 self.gPodderOpmlLister.set_title(self.custom_title)
3398 if hasattr(self, 'hide_url_entry'):
3399 self.hbox25.hide_all()
3401 self.setup_treeview(self.treeviewChannelChooser)
3402 self.setup_treeview(self.treeviewTopPodcastsChooser)
3403 self.setup_treeview(self.treeviewYouTubeChooser)
3405 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
3407 def setup_treeview(self, tv):
3408 togglecell = gtk.CellRendererToggle()
3409 togglecell.set_property( 'activatable', True)
3410 togglecell.connect( 'toggled', self.callback_edited)
3411 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3413 titlecell = gtk.CellRendererText()
3414 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3415 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3417 for itemcolumn in ( togglecolumn, titlecolumn ):
3418 tv.append_column(itemcolumn)
3420 def callback_edited( self, cell, path):
3421 model = self.get_treeview().get_model()
3423 url = model[path][2]
3425 model[path][0] = not model[path][0]
3426 if model[path][0]:
3427 self.channels.append( url)
3428 else:
3429 self.channels.remove( url)
3431 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
3433 def get_selected_channels(self, tab=None):
3434 channels = []
3436 model = self.get_treeview(tab).get_model()
3437 if model is not None:
3438 for row in model:
3439 if row[0]:
3440 channels.append(row[2])
3442 return channels
3444 def on_change_tab(self, tab):
3445 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
3447 def thread_finished(self, model, tab=0):
3448 if tab == 1:
3449 tv = self.treeviewTopPodcastsChooser
3450 elif tab == 2:
3451 tv = self.treeviewYouTubeChooser
3452 self.entryYoutubeSearch.set_sensitive(True)
3453 self.btnSearchYouTube.set_sensitive(True)
3454 self.btnOK.set_sensitive(False)
3455 else:
3456 tv = self.treeviewChannelChooser
3457 self.btnDownloadOpml.set_sensitive(True)
3458 self.entryURL.set_sensitive(True)
3459 self.channels = []
3461 tv.set_model(model)
3462 tv.set_sensitive(True)
3464 def thread_func(self, tab=0):
3465 if tab == 1:
3466 model = opml.Importer(gl.config.toplist_url).get_model()
3467 if len(model) == 0:
3468 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3469 elif tab == 2:
3470 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
3471 if len(model) == 0:
3472 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
3473 else:
3474 model = opml.Importer(self.entryURL.get_text()).get_model()
3475 if len(model) == 0:
3476 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3478 util.idle_add(self.thread_finished, model, tab)
3480 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3481 if callback_for_channel:
3482 self.callback_for_channel = callback_for_channel
3483 if callback_finished:
3484 self.callback_finished = callback_finished
3485 self.entryURL.set_text( url)
3486 self.btnDownloadOpml.set_sensitive( False)
3487 self.entryURL.set_sensitive( False)
3488 self.btnOK.set_sensitive( False)
3489 self.treeviewChannelChooser.set_sensitive( False)
3490 Thread( target = self.thread_func).start()
3491 Thread( target = lambda: self.thread_func(1)).start()
3493 def select_all( self, value ):
3494 enabled = False
3495 model = self.get_treeview().get_model()
3496 if model is not None:
3497 for row in model:
3498 row[0] = value
3499 if value:
3500 enabled = True
3501 self.btnOK.set_sensitive(enabled)
3503 def on_gPodderOpmlLister_destroy(self, widget, *args):
3504 pass
3506 def on_btnDownloadOpml_clicked(self, widget, *args):
3507 self.get_channels_from_url( self.entryURL.get_text())
3509 def on_btnSearchYouTube_clicked(self, widget, *args):
3510 self.entryYoutubeSearch.set_sensitive(False)
3511 self.treeviewYouTubeChooser.set_sensitive(False)
3512 self.btnSearchYouTube.set_sensitive(False)
3513 Thread(target = lambda: self.thread_func(2)).start()
3515 def on_btnSelectAll_clicked(self, widget, *args):
3516 self.select_all(True)
3518 def on_btnSelectNone_clicked(self, widget, *args):
3519 self.select_all(False)
3521 def on_btnOK_clicked(self, widget, *args):
3522 self.channels = self.get_selected_channels()
3523 self.gPodderOpmlLister.destroy()
3525 # add channels that have been selected
3526 for url in self.channels:
3527 if self.callback_for_channel:
3528 self.callback_for_channel( url)
3530 if self.callback_finished:
3531 util.idle_add(self.callback_finished)
3533 def on_btnCancel_clicked(self, widget, *args):
3534 self.gPodderOpmlLister.destroy()
3536 def on_entryYoutubeSearch_key_press_event(self, widget, event):
3537 if event.keyval == gtk.keysyms.Return:
3538 self.on_btnSearchYouTube_clicked(widget)
3540 def get_treeview(self, tab=None):
3541 if tab is None:
3542 tab = self.notebookChannelAdder.get_current_page()
3544 if tab == 0:
3545 return self.treeviewChannelChooser
3546 elif tab == 1:
3547 return self.treeviewTopPodcastsChooser
3548 else:
3549 return self.treeviewYouTubeChooser
3551 class gPodderEpisodeSelector( GladeWidget):
3552 """Episode selection dialog
3554 Optional keyword arguments that modify the behaviour of this dialog:
3556 - callback: Function that takes 1 parameter which is a list of
3557 the selected episodes (or empty list when none selected)
3558 - remove_callback: Function that takes 1 parameter which is a list
3559 of episodes that should be "removed" (see below)
3560 (default is None, which means remove not possible)
3561 - remove_action: Label for the "remove" action (default is "Remove")
3562 - remove_finished: Callback after all remove callbacks have finished
3563 (default is None, also depends on remove_callback)
3564 - episodes: List of episodes that are presented for selection
3565 - selected: (optional) List of boolean variables that define the
3566 default checked state for the given episodes
3567 - selected_default: (optional) The default boolean value for the
3568 checked state if no other value is set
3569 (default is False)
3570 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3571 columns, the name is the attribute name of the episode to be
3572 read from each episode object. The sort name is the
3573 attribute name of the episode to be used to sort this column.
3574 If the sort_name is None it will use the attribute name for
3575 sorting. The sort type is the type of the sort column.
3576 The caption attribute is the text that appear as column caption
3577 (default is [('title_and_description', None, None, 'Episode'),])
3578 - title: (optional) The title of the window + heading
3579 - instructions: (optional) A one-line text describing what the
3580 user should select / what the selection is for
3581 - stock_ok_button: (optional) Will replace the "OK" button with
3582 another GTK+ stock item to be used for the
3583 affirmative button of the dialog (e.g. can
3584 be gtk.STOCK_DELETE when the episodes to be
3585 selected will be deleted after closing the
3586 dialog)
3587 - selection_buttons: (optional) A dictionary with labels as
3588 keys and callbacks as values; for each
3589 key a button will be generated, and when
3590 the button is clicked, the callback will
3591 be called for each episode and the return
3592 value of the callback (True or False) will
3593 be the new selected state of the episode
3594 - size_attribute: (optional) The name of an attribute of the
3595 supplied episode objects that can be used to
3596 calculate the size of an episode; set this to
3597 None if no total size calculation should be
3598 done (in cases where total size is useless)
3599 (default is 'length')
3600 - tooltip_attribute: (optional) The name of an attribute of
3601 the supplied episode objects that holds
3602 the text for the tooltips when hovering
3603 over an episode (default is 'description')
3606 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3608 COLUMN_INDEX = 0
3609 COLUMN_TOOLTIP = 1
3610 COLUMN_TOGGLE = 2
3611 COLUMN_ADDITIONAL = 3
3613 def new( self):
3614 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
3615 if not hasattr( self, 'callback'):
3616 self.callback = None
3618 if not hasattr(self, 'remove_callback'):
3619 self.remove_callback = None
3621 if not hasattr(self, 'remove_action'):
3622 self.remove_action = _('Remove')
3624 if not hasattr(self, 'remove_finished'):
3625 self.remove_finished = None
3627 if not hasattr( self, 'episodes'):
3628 self.episodes = []
3630 if not hasattr( self, 'size_attribute'):
3631 self.size_attribute = 'length'
3633 if not hasattr(self, 'tooltip_attribute'):
3634 self.tooltip_attribute = 'description'
3636 if not hasattr( self, 'selection_buttons'):
3637 self.selection_buttons = {}
3639 if not hasattr( self, 'selected_default'):
3640 self.selected_default = False
3642 if not hasattr( self, 'selected'):
3643 self.selected = [self.selected_default]*len(self.episodes)
3645 if len(self.selected) < len(self.episodes):
3646 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3648 if not hasattr( self, 'columns'):
3649 self.columns = (('title_and_description', None, None, _('Episode')),)
3651 if hasattr( self, 'title'):
3652 self.gPodderEpisodeSelector.set_title( self.title)
3653 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3655 if gpodder.interface == gpodder.MAEMO:
3656 self.labelHeading.hide()
3658 if hasattr( self, 'instructions'):
3659 self.labelInstructions.set_text( self.instructions)
3660 self.labelInstructions.show_all()
3662 if hasattr(self, 'stock_ok_button'):
3663 if self.stock_ok_button == 'gpodder-download':
3664 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3665 self.btnOK.set_label(_('Download'))
3666 else:
3667 self.btnOK.set_label(self.stock_ok_button)
3668 self.btnOK.set_use_stock(True)
3670 # check/uncheck column
3671 toggle_cell = gtk.CellRendererToggle()
3672 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3673 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3675 next_column = self.COLUMN_ADDITIONAL
3676 for name, sort_name, sort_type, caption in self.columns:
3677 renderer = gtk.CellRendererText()
3678 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3679 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3680 column.set_resizable( True)
3681 # Only set "expand" on the first column (so more text is displayed there)
3682 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3683 if sort_name is not None:
3684 column.set_sort_column_id(next_column+1)
3685 else:
3686 column.set_sort_column_id(next_column)
3687 self.treeviewEpisodes.append_column( column)
3688 next_column += 1
3690 if sort_name is not None:
3691 # add the sort column
3692 column = gtk.TreeViewColumn()
3693 column.set_visible(False)
3694 self.treeviewEpisodes.append_column( column)
3695 next_column += 1
3697 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3698 # add string column type plus sort column type if it exists
3699 for name, sort_name, sort_type, caption in self.columns:
3700 column_types.append(gobject.TYPE_STRING)
3701 if sort_name is not None:
3702 column_types.append(sort_type)
3703 self.model = gtk.ListStore( *column_types)
3705 tooltip = None
3706 for index, episode in enumerate( self.episodes):
3707 if self.tooltip_attribute is not None:
3708 try:
3709 tooltip = getattr(episode, self.tooltip_attribute)
3710 except:
3711 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3712 tooltip = None
3713 row = [ index, tooltip, self.selected[index] ]
3714 for name, sort_name, sort_type, caption in self.columns:
3715 if not hasattr(episode, name):
3716 log('Warning: Missing attribute "%s"', name, sender=self)
3717 row.append(None)
3718 else:
3719 row.append(getattr( episode, name))
3721 if sort_name is not None:
3722 if not hasattr(episode, sort_name):
3723 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3724 row.append(None)
3725 else:
3726 row.append(getattr( episode, sort_name))
3727 self.model.append( row)
3729 if self.remove_callback is not None:
3730 self.btnRemoveAction.show()
3731 self.btnRemoveAction.set_label(self.remove_action)
3733 # connect to tooltip signals
3734 if self.tooltip_attribute is not None:
3735 try:
3736 self.treeviewEpisodes.set_property('has-tooltip', True)
3737 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3738 except:
3739 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3740 self.last_tooltip_episode = None
3741 self.episode_list_can_tooltip = True
3743 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3744 self.treeviewEpisodes.set_rules_hint( True)
3745 self.treeviewEpisodes.set_model( self.model)
3746 self.treeviewEpisodes.columns_autosize()
3747 self.calculate_total_size()
3749 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3750 # With get_bin_window, we get the window that contains the rows without
3751 # the header. The Y coordinate of this window will be the height of the
3752 # treeview header. This is the amount we have to subtract from the
3753 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3754 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3755 y -= x_bin
3756 y -= y_bin
3757 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3759 if not self.episode_list_can_tooltip:
3760 self.last_tooltip_episode = None
3761 return False
3763 if path is not None:
3764 model = treeview.get_model()
3765 iter = model.get_iter(path)
3766 index = model.get_value(iter, self.COLUMN_INDEX)
3767 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3768 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3769 self.last_tooltip_episode = None
3770 return False
3771 self.last_tooltip_episode = index
3773 if description is not None:
3774 tooltip.set_text(description)
3775 return True
3776 else:
3777 return False
3779 self.last_tooltip_episode = None
3780 return False
3782 def treeview_episodes_button_pressed(self, treeview, event):
3783 if event.button == 3:
3784 menu = gtk.Menu()
3786 if len(self.selection_buttons):
3787 for label in self.selection_buttons:
3788 item = gtk.MenuItem(label)
3789 item.connect('activate', self.custom_selection_button_clicked, label)
3790 menu.append(item)
3791 menu.append(gtk.SeparatorMenuItem())
3793 item = gtk.MenuItem(_('Select all'))
3794 item.connect('activate', self.on_btnCheckAll_clicked)
3795 menu.append(item)
3797 item = gtk.MenuItem(_('Select none'))
3798 item.connect('activate', self.on_btnCheckNone_clicked)
3799 menu.append(item)
3801 menu.show_all()
3802 # Disable tooltips while we are showing the menu, so
3803 # the tooltip will not appear over the menu
3804 self.episode_list_can_tooltip = False
3805 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3806 menu.popup(None, None, None, event.button, event.time)
3808 return True
3810 def episode_list_allow_tooltips(self):
3811 self.episode_list_can_tooltip = True
3813 def calculate_total_size( self):
3814 if self.size_attribute is not None:
3815 (total_size, count) = (0, 0)
3816 for episode in self.get_selected_episodes():
3817 try:
3818 total_size += int(getattr( episode, self.size_attribute))
3819 count += 1
3820 except:
3821 log( 'Cannot get size for %s', episode.title, sender = self)
3823 text = []
3824 if count == 0:
3825 text.append(_('Nothing selected'))
3826 elif count == 1:
3827 text.append(_('One episode selected'))
3828 else:
3829 text.append(_('%d episodes selected') % count)
3830 if total_size > 0:
3831 text.append(_('total size: %s') % gl.format_filesize(total_size))
3832 self.labelTotalSize.set_text(', '.join(text))
3833 self.btnOK.set_sensitive(count>0)
3834 self.btnRemoveAction.set_sensitive(count>0)
3835 if count > 0:
3836 self.btnCancel.set_label(gtk.STOCK_CANCEL)
3837 else:
3838 self.btnCancel.set_label(gtk.STOCK_CLOSE)
3839 else:
3840 self.btnOK.set_sensitive(False)
3841 self.btnRemoveAction.set_sensitive(False)
3842 for index, row in enumerate(self.model):
3843 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3844 self.btnOK.set_sensitive(True)
3845 self.btnRemoveAction.set_sensitive(True)
3846 break
3847 self.labelTotalSize.set_text('')
3849 def toggle_cell_handler( self, cell, path):
3850 model = self.treeviewEpisodes.get_model()
3851 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3853 self.calculate_total_size()
3855 def custom_selection_button_clicked(self, button, label):
3856 callback = self.selection_buttons[label]
3858 for index, row in enumerate( self.model):
3859 new_value = callback( self.episodes[index])
3860 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3862 self.calculate_total_size()
3864 def on_btnCheckAll_clicked( self, widget):
3865 for row in self.model:
3866 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3868 self.calculate_total_size()
3870 def on_btnCheckNone_clicked( self, widget):
3871 for row in self.model:
3872 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3874 self.calculate_total_size()
3876 def on_remove_action_activate(self, widget):
3877 episodes = self.get_selected_episodes(remove_episodes=True)
3879 for episode in episodes:
3880 self.remove_callback(episode)
3882 if self.remove_finished is not None:
3883 self.remove_finished()
3884 self.calculate_total_size()
3886 def get_selected_episodes( self, remove_episodes=False):
3887 selected_episodes = []
3889 for index, row in enumerate( self.model):
3890 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3891 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
3893 if remove_episodes:
3894 for episode in selected_episodes:
3895 index = self.episodes.index(episode)
3896 iter = self.model.get_iter_first()
3897 while iter is not None:
3898 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
3899 self.model.remove(iter)
3900 break
3901 iter = self.model.iter_next(iter)
3903 return selected_episodes
3905 def on_btnOK_clicked( self, widget):
3906 self.gPodderEpisodeSelector.destroy()
3907 if self.callback is not None:
3908 self.callback( self.get_selected_episodes())
3910 def on_btnCancel_clicked( self, widget):
3911 self.gPodderEpisodeSelector.destroy()
3912 if self.callback is not None:
3913 self.callback([])
3915 class gPodderConfigEditor(GladeWidget):
3916 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3918 def new(self):
3919 name_column = gtk.TreeViewColumn(_('Setting'))
3920 name_renderer = gtk.CellRendererText()
3921 name_column.pack_start(name_renderer)
3922 name_column.add_attribute(name_renderer, 'text', 0)
3923 name_column.add_attribute(name_renderer, 'style', 5)
3924 self.configeditor.append_column(name_column)
3926 value_column = gtk.TreeViewColumn(_('Set to'))
3927 value_check_renderer = gtk.CellRendererToggle()
3928 value_column.pack_start(value_check_renderer, expand=False)
3929 value_column.add_attribute(value_check_renderer, 'active', 7)
3930 value_column.add_attribute(value_check_renderer, 'visible', 6)
3931 value_column.add_attribute(value_check_renderer, 'activatable', 6)
3932 value_check_renderer.connect('toggled', self.value_toggled)
3934 value_renderer = gtk.CellRendererText()
3935 value_column.pack_start(value_renderer)
3936 value_column.add_attribute(value_renderer, 'text', 2)
3937 value_column.add_attribute(value_renderer, 'visible', 4)
3938 value_column.add_attribute(value_renderer, 'editable', 4)
3939 value_column.add_attribute(value_renderer, 'style', 5)
3940 value_renderer.connect('edited', self.value_edited)
3941 self.configeditor.append_column(value_column)
3943 self.model = gl.config.model()
3944 self.filter = self.model.filter_new()
3945 self.filter.set_visible_func(self.visible_func)
3947 self.configeditor.set_model(self.filter)
3948 self.configeditor.set_rules_hint(True)
3950 def visible_func(self, model, iter, user_data=None):
3951 text = self.entryFilter.get_text().lower()
3952 if text == '':
3953 return True
3954 else:
3955 # either the variable name or its value
3956 return (text in model.get_value(iter, 0).lower() or
3957 text in model.get_value(iter, 2).lower())
3959 def value_edited(self, renderer, path, new_text):
3960 model = self.configeditor.get_model()
3961 iter = model.get_iter(path)
3962 name = model.get_value(iter, 0)
3963 type_cute = model.get_value(iter, 1)
3965 if not gl.config.update_field(name, new_text):
3966 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))
3968 def value_toggled(self, renderer, path):
3969 model = self.configeditor.get_model()
3970 iter = model.get_iter(path)
3971 field_name = model.get_value(iter, 0)
3972 field_type = model.get_value(iter, 3)
3974 # Flip the boolean config flag
3975 if field_type == bool:
3976 gl.config.toggle_flag(field_name)
3978 def on_entryFilter_changed(self, widget):
3979 self.filter.refilter()
3981 def on_btnShowAll_clicked(self, widget):
3982 self.entryFilter.set_text('')
3983 self.entryFilter.grab_focus()
3985 def on_btnClose_clicked(self, widget):
3986 self.gPodderConfigEditor.destroy()
3988 class gPodderPlaylist(GladeWidget):
3989 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
3991 def new(self):
3992 self.m3u_header = '#EXTM3U\n'
3993 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
3994 if self.mountpoint == '/':
3995 self.mountpoint = gl.config.mp3_player_folder
3996 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
3997 self.playlist_file = os.path.join(self.mountpoint,
3998 gl.config.mp3_player_playlist_file)
3999 icon_theme = gtk.icon_theme_get_default()
4000 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4002 # add column two
4003 check_cell = gtk.CellRendererToggle()
4004 check_cell.set_property('activatable', True)
4005 check_cell.connect('toggled', self.cell_toggled)
4006 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4007 self.treeviewPlaylist.append_column(check_column)
4009 # add column three
4010 column = gtk.TreeViewColumn(_('Filename'))
4011 icon_cell = gtk.CellRendererPixbuf()
4012 column.pack_start(icon_cell, False)
4013 column.add_attribute(icon_cell, 'pixbuf', 0)
4014 filename_cell = gtk.CellRendererText()
4015 column.pack_start(filename_cell, True)
4016 column.add_attribute(filename_cell, 'text', 2)
4018 column.set_resizable(True)
4019 self.treeviewPlaylist.append_column(column)
4021 # Make treeview reorderable
4022 self.treeviewPlaylist.set_reorderable(True)
4024 # init liststore
4025 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4026 self.treeviewPlaylist.set_model(self.playlist)
4028 # read device and playlist and fill the TreeView
4029 self.m3u = self.read_m3u()
4030 self.device = self.read_device()
4031 self.write2gui()
4033 def cell_toggled(self, cellrenderertoggle, path):
4034 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4035 it = liststore.get_iter(path)
4036 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4038 def on_btnCancelPlaylist_clicked(self, widget):
4039 self.gPodderPlaylist.destroy()
4041 def on_btnSavePlaylist_clicked(self, widget):
4042 self.write_m3u()
4043 self.gPodderPlaylist.destroy()
4045 def read_m3u(self):
4047 read all files from the existing playlist
4049 tracks = []
4050 if os.path.exists(self.playlist_file):
4051 for line in open(self.playlist_file, 'r'):
4052 if line != self.m3u_header:
4053 if line.startswith('#'):
4054 tracks.append([False, line[1:].strip()])
4055 else:
4056 tracks.append([True, line.strip()])
4057 return tracks
4059 def write_m3u(self):
4061 write the list into the playlist on the device
4063 playlist_folder = os.path.split(self.playlist_file)[0]
4064 if not util.make_directory(playlist_folder):
4065 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4066 else:
4067 try:
4068 fp = open(self.playlist_file, 'w')
4069 fp.write(self.m3u_header)
4070 for icon, checked, filename in self.playlist:
4071 if not checked:
4072 fp.write('#')
4073 fp.write(filename)
4074 fp.write('\n')
4075 fp.close()
4076 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4077 except IOError, ioe:
4078 self.show_message(str(ioe), _('Error writing playlist file'))
4080 def read_device(self):
4082 read all files from the device
4084 tracks = []
4085 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4086 for file in files:
4087 filename = os.path.join(root, file)
4089 if filename == self.playlist_file:
4090 # We don't want to have our playlist file as
4091 # an entry in our file list, so skip it!
4092 break
4094 if not gl.config.mp3_player_playlist_absolute_path:
4095 filename = filename[len(self.mountpoint):]
4097 if gl.config.mp3_player_playlist_win_path:
4098 filename = filename.replace( '/', '\\')
4100 tracks.append(filename)
4101 return tracks
4103 def write2gui(self):
4104 # add the files from the device to the list only when
4105 # they are not yet in the playlist
4106 # mark this files as NEW
4107 for filename in self.device[:]:
4108 m3ulist = [file[1] for file in self.m3u]
4109 if filename not in m3ulist:
4110 self.playlist.append([self.icon_new, False, filename])
4112 # add the files from the playlist to the list only when
4113 # they are on the device
4114 for checked, filename in self.m3u[:]:
4115 if filename in self.device:
4116 self.playlist.append([None, checked, filename])
4118 class gPodderDependencyManager(GladeWidget):
4119 def new(self):
4120 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4121 self.treeview_components.append_column(col_name)
4122 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4123 self.treeview_components.append_column(col_installed)
4124 self.treeview_components.set_model(services.dependency_manager.get_model())
4125 self.btn_about.set_sensitive(False)
4127 def on_btn_about_clicked(self, widget):
4128 selection = self.treeview_components.get_selection()
4129 model, iter = selection.get_selected()
4130 if iter is not None:
4131 title = model.get_value(iter, 0)
4132 description = model.get_value(iter, 1)
4133 available = model.get_value(iter, 3)
4134 missing = model.get_value(iter, 4)
4136 if not available:
4137 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4139 self.show_message(description, title)
4141 def on_btn_install_clicked(self, widget):
4142 # TODO: Implement package manager integration
4143 pass
4145 def on_treeview_components_cursor_changed(self, treeview):
4146 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4147 # TODO: If installing is possible, enable btn_install
4149 def on_gPodderDependencyManager_response(self, dialog, response_id):
4150 self.gPodderDependencyManager.destroy()
4152 class gPodderWelcome(GladeWidget):
4153 def new(self):
4154 pass
4156 def on_show_example_podcasts(self, button):
4157 self.gPodderWelcome.destroy()
4158 self.show_example_podcasts_callback(None)
4160 def on_setup_my_gpodder(self, gpodder):
4161 self.gPodderWelcome.destroy()
4162 self.setup_my_gpodder_callback(None)
4164 def on_btnCancel_clicked(self, button):
4165 self.gPodderWelcome.destroy()
4167 def main():
4168 gobject.threads_init()
4169 gtk.window_set_default_icon_name( 'gpodder')
4171 gPodder().run()