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