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/>.
36 from xml
.sax
import saxutils
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 def __init__(self
, *args
, **kwargs
):
56 def method(interface
):
59 def __init__(self
, *args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
66 from gpodder
import feedcore
67 from gpodder
import util
68 from gpodder
import opml
69 from gpodder
import download
70 from gpodder
import my
71 from gpodder
.liblogger
import log
75 from gpodder
.model
import PodcastChannel
76 from gpodder
.dbsqlite
import Database
78 from gpodder
.gtkui
.model
import PodcastListModel
79 from gpodder
.gtkui
.model
import EpisodeListModel
80 from gpodder
.gtkui
.config
import UIConfig
81 from gpodder
.gtkui
.download
import DownloadStatusModel
82 from gpodder
.gtkui
.services
import CoverDownloader
83 from gpodder
.gtkui
.widgets
import SimpleMessageArea
84 from gpodder
.gtkui
.desktopfile
import UserAppsReader
86 from gpodder
.gtkui
.draw
import draw_text_box_centered
88 from gpodder
.gtkui
.interface
.common
import BuilderWidget
89 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
90 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
92 if gpodder
.ui
.desktop
:
93 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
95 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
96 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
97 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
98 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
99 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
100 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
102 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
104 except Exception, exc
:
105 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
106 log('Warning: This probably means your PyGTK installation is too old!')
107 have_trayicon
= False
108 elif gpodder
.ui
.diablo
:
109 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
110 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
111 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
112 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
113 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
114 have_trayicon
= False
115 elif gpodder
.ui
.fremantle
:
116 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
117 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
118 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
119 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
120 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
121 from gpodder
.gtkui
.frmntl
.podcasts
import gPodderPodcasts
122 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
123 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
124 from gpodder
.gtkui
.interface
.common
import Orientation
125 have_trayicon
= False
127 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
129 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
130 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
135 class gPodder(BuilderWidget
, dbus
.service
.Object
):
136 finger_friendly_widgets
= ['btnCleanUpDownloads']
138 def __init__(self
, bus_name
, config
):
139 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
140 self
.db
= Database(gpodder
.database_file
)
142 BuilderWidget
.__init
__(self
, None)
145 if gpodder
.ui
.diablo
:
147 self
.app
= hildon
.Program()
148 self
.app
.add_window(self
.main_window
)
149 self
.main_window
.add_toolbar(self
.toolbar
)
151 for child
in self
.main_menu
.get_children():
153 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
154 self
.bluetooth_available
= False
155 elif gpodder
.ui
.fremantle
:
157 self
.app
= hildon
.Program()
158 self
.app
.add_window(self
.main_window
)
160 appmenu
= hildon
.AppMenu()
161 for action
in (self
.itemUpdate
, \
162 self
.itemRemoveOldEpisodes
, \
164 button
= gtk
.Button()
165 action
.connect_proxy(button
)
166 appmenu
.append(button
)
168 self
.main_window
.set_app_menu(appmenu
)
169 self
._fremantle
_update
_banner
= None
171 # Initialize portrait mode / rotation manager
172 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
173 self
.main_window
, gpodder
.__version
__)
175 self
.bluetooth_available
= False
177 self
.bluetooth_available
= util
.bluetooth_available()
178 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
180 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
181 if not gpodder
.ui
.fremantle
:
182 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
183 self
.main_window
.show()
185 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
187 self
.config
.add_observer(self
.on_config_changed
)
189 self
.tray_icon
= None
190 self
.episode_shownotes_window
= None
191 self
.new_episodes_window
= None
193 if gpodder
.ui
.desktop
:
194 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
195 self
.main_window
, self
.show_confirmation
, \
196 self
.update_episode_list_icons
, \
197 self
.update_podcast_list_model
, self
.toolPreferences
, \
198 gPodderEpisodeSelector
)
202 self
.download_status_model
= DownloadStatusModel()
203 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
205 if gpodder
.ui
.desktop
:
206 self
.show_hide_tray_icon()
207 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
208 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
210 if not gpodder
.ui
.fremantle
:
211 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
212 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
213 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
214 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
216 # When the amount of maximum downloads changes, notify the queue manager
217 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_and_retire_threads()
218 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
220 self
.default_title
= 'gPodder'
221 if gpodder
.__version
__.rfind('git') != -1:
222 self
.set_title('gPodder %s' % gpodder
.__version
__)
224 title
= self
.gPodder
.get_title()
225 if title
is not None:
226 self
.set_title(title
)
228 self
.set_title(_('gPodder'))
230 self
.cover_downloader
= CoverDownloader()
232 # Generate list models for podcasts and their episodes
233 self
.podcast_list_model
= PodcastListModel(self
.config
.podcast_list_icon_size
, self
.cover_downloader
)
235 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
236 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
238 if gpodder
.ui
.fremantle
:
239 self
.button_subscribe
.set_name('HildonButton-thumb')
240 self
.button_podcasts
.set_name('HildonButton-thumb')
241 self
.button_downloads
.set_name('HildonButton-thumb')
243 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
244 while gtk
.events_pending():
245 gtk
.main_iteration(False)
247 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
248 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
249 show_episode_shownotes
=self
.show_episode_shownotes
, \
250 update_podcast_list_model
=self
.update_podcast_list_model
, \
251 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
252 item_view_episodes_all
=self
.item_view_episodes_all
, \
253 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
254 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
255 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
)
257 def on_podcast_selected(channel
):
258 self
.active_channel
= channel
259 self
.update_episode_list_model()
260 self
.episodes_window
.channel
= self
.active_channel
261 self
.episodes_window
.show()
263 self
.podcasts_window
= gPodderPodcasts(self
.main_window
, \
264 show_podcast_episodes
=on_podcast_selected
, \
265 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
266 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
267 item_view_podcasts_all
=self
.item_view_podcasts_all
, \
268 item_view_podcasts_downloaded
=self
.item_view_podcasts_downloaded
, \
269 item_view_podcasts_unplayed
=self
.item_view_podcasts_unplayed
)
271 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
272 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
273 on_btnCleanUpDownloads_clicked
=self
.on_btnCleanUpDownloads_clicked
, \
274 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
275 downloads_list_get_selection
=self
.downloads_list_get_selection
)
276 self
.treeChannels
= self
.podcasts_window
.treeview
277 self
.treeAvailable
= self
.episodes_window
.treeview
278 self
.treeDownloads
= self
.downloads_window
.treeview
280 # Init the treeviews that we use
281 self
.init_podcast_list_treeview()
282 self
.init_episode_list_treeview()
283 self
.init_download_list_treeview()
285 if self
.config
.podcast_list_hide_boring
:
286 self
.item_view_hide_boring_podcasts
.set_active(True)
288 self
.currently_updating
= False
291 self
.context_menu_mouse_button
= 1
293 self
.context_menu_mouse_button
= 3
295 if self
.config
.start_iconified
:
296 self
.iconify_main_window()
298 self
.download_tasks_seen
= set()
299 self
.download_list_update_enabled
= False
300 self
.last_download_count
= 0
302 # Subscribed channels
303 self
.active_channel
= None
304 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
305 self
.channel_list_changed
= True
306 self
.update_podcasts_tab()
308 # load list of user applications for audio playback
309 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
311 time
.sleep(3) # give other parts of gpodder a chance to start up
312 self
.user_apps_reader
.read()
313 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
314 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
315 threading
.Thread(target
=read_apps
).start()
317 # Set the "Device" menu item for the first time
318 if gpodder
.ui
.desktop
:
319 self
.update_item_device()
321 # Now, update the feed cache, when everything's in place
322 if not gpodder
.ui
.fremantle
:
323 self
.btnUpdateFeeds
.show()
324 self
.updating_feed_cache
= False
325 self
.feed_cache_update_cancelled
= False
326 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
328 # Look for partial file downloads
329 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
332 self
.message_area
= None
334 resumable_episodes
= []
335 if len(partial_files
) > 0:
336 for f
in partial_files
:
337 correct_name
= f
[:-len('.partial')] # strip ".partial"
338 log('Searching episode for file: %s', correct_name
, sender
=self
)
339 found_episode
= False
340 for c
in self
.channels
:
341 for e
in c
.get_all_episodes():
342 if e
.local_filename(create
=False, check_only
=True) == correct_name
:
343 log('Found episode: %s', e
.title
, sender
=self
)
344 resumable_episodes
.append(e
)
350 if not found_episode
:
351 log('Partial file without episode: %s', f
, sender
=self
)
354 if len(resumable_episodes
):
355 self
.download_episode_list_paused(resumable_episodes
)
356 if not gpodder
.ui
.fremantle
:
357 self
.message_area
= SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
358 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
359 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
360 self
.message_area
.show_all()
361 self
.wNotebook
.set_current_page(1)
363 self
.clean_up_downloads(delete_partial
=False)
365 self
.clean_up_downloads(delete_partial
=True)
367 # Start the auto-update procedure
368 self
.auto_update_procedure(first_run
=True)
370 # Delete old episodes if the user wishes to
371 if self
.config
.auto_remove_old_episodes
:
372 old_episodes
= self
.get_old_episodes()
373 if len(old_episodes
) > 0:
374 self
.delete_episode_list(old_episodes
, confirm
=False)
375 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
377 if gpodder
.ui
.fremantle
:
378 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
379 self
.button_subscribe
.set_sensitive(True)
380 self
.button_podcasts
.set_sensitive(True)
381 self
.button_downloads
.set_sensitive(True)
383 # First-time users should be asked if they want to see the OPML
384 if not self
.channels
and not gpodder
.ui
.fremantle
:
385 util
.idle_add(self
.on_itemUpdate_activate
)
387 def on_button_subscribe_clicked(self
, button
):
388 self
.on_itemImportChannels_activate(button
)
390 def on_button_podcasts_clicked(self
, widget
):
392 self
.podcasts_window
.show()
394 gPodderWelcome(self
.gPodder
, \
395 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
, \
396 setup_my_gpodder_callback
=self
.on_download_from_mygpo
)
398 def on_button_downloads_clicked(self
, widget
):
399 self
.downloads_window
.show()
401 def on_window_orientation_changed(self
, orientation
):
402 old_container
= self
.main_window
.get_child()
403 if orientation
== Orientation
.PORTRAIT
:
404 container
= gtk
.VButtonBox()
406 container
= gtk
.HButtonBox()
407 container
.set_layout(old_container
.get_layout())
408 for child
in old_container
.get_children():
409 if orientation
== Orientation
.LANDSCAPE
:
410 child
.set_alignment(0.5, 0.5, 0., 0.)
411 child
.set_property('width-request', 200)
413 child
.set_alignment(0.5, 0.5, .9, 0.)
414 child
.set_property('width-request', 350)
415 child
.reparent(container
)
417 self
.buttonbox
= container
418 self
.main_window
.remove(old_container
)
419 self
.main_window
.add(container
)
421 def on_treeview_podcasts_selection_changed(self
, selection
):
422 model
, iter = selection
.get_selected()
424 self
.active_channel
= None
425 self
.episode_list_model
.clear()
427 def on_treeview_button_pressed(self
, treeview
, event
):
428 if event
.window
!= treeview
.get_bin_window():
431 TreeViewHelper
.save_button_press_event(treeview
, event
)
433 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
434 TreeViewHelper
.ROLE_PODCASTS
:
435 return self
.currently_updating
437 return event
.button
== self
.context_menu_mouse_button
and \
440 def on_treeview_podcasts_button_released(self
, treeview
, event
):
441 if event
.window
!= treeview
.get_bin_window():
445 return self
.treeview_channels_handle_gestures(treeview
, event
)
447 return self
.treeview_channels_show_context_menu(treeview
, event
)
449 def on_treeview_episodes_button_released(self
, treeview
, event
):
450 if event
.window
!= treeview
.get_bin_window():
454 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
455 return self
.treeview_available_handle_gestures(treeview
, event
)
457 return self
.treeview_available_show_context_menu(treeview
, event
)
459 def on_treeview_downloads_button_released(self
, treeview
, event
):
460 if event
.window
!= treeview
.get_bin_window():
463 return self
.treeview_downloads_show_context_menu(treeview
, event
)
465 def init_podcast_list_treeview(self
):
466 # Set up podcast channel tree view widget
467 self
.treeChannels
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(PodcastListModel
))
469 if gpodder
.ui
.fremantle
:
470 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
471 self
.item_view_podcasts_downloaded
.set_active(True)
472 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
473 self
.item_view_podcasts_unplayed
.set_active(True)
475 self
.item_view_podcasts_all
.set_active(True)
476 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
478 iconcolumn
= gtk
.TreeViewColumn('')
479 iconcell
= gtk
.CellRendererPixbuf()
480 iconcolumn
.pack_start(iconcell
, False)
481 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
482 self
.treeChannels
.append_column(iconcolumn
)
484 namecolumn
= gtk
.TreeViewColumn('')
485 namecell
= gtk
.CellRendererText()
486 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
487 namecolumn
.pack_start(namecell
, True)
488 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
490 iconcell
= gtk
.CellRendererPixbuf()
491 iconcell
.set_property('xalign', 1.0)
492 namecolumn
.pack_start(iconcell
, False)
493 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
494 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
495 self
.treeChannels
.append_column(namecolumn
)
497 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
499 # When no podcast is selected, clear the episode list model
500 selection
= self
.treeChannels
.get_selection()
501 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
503 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
505 def init_episode_list_treeview(self
):
506 self
.episode_list_model
= EpisodeListModel()
508 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
509 self
.item_view_episodes_undeleted
.set_active(True)
510 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
511 self
.item_view_episodes_downloaded
.set_active(True)
512 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
513 self
.item_view_episodes_unplayed
.set_active(True)
515 self
.item_view_episodes_all
.set_active(True)
517 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
519 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
521 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
523 iconcell
= gtk
.CellRendererPixbuf()
525 iconcell
.set_fixed_size(50, 50)
526 status_column_label
= ''
528 status_column_label
= _('Status')
529 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
531 namecell
= gtk
.CellRendererText()
532 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
533 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
534 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
535 namecolumn
.set_resizable(True)
536 namecolumn
.set_expand(True)
538 sizecell
= gtk
.CellRendererText()
539 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
541 releasecell
= gtk
.CellRendererText()
542 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
544 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
545 itemcolumn
.set_reorderable(True)
546 self
.treeAvailable
.append_column(itemcolumn
)
549 sizecolumn
.set_visible(False)
550 releasecolumn
.set_visible(False)
552 self
.treeAvailable
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(EpisodeListModel
))
554 selection
= self
.treeAvailable
.get_selection()
555 if gpodder
.ui
.diablo
:
556 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
557 selection
.set_mode(gtk
.SELECTION_SINGLE
)
559 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
560 elif gpodder
.ui
.fremantle
:
561 selection
.set_mode(gtk
.SELECTION_SINGLE
)
563 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
564 # Update the sensitivity of the toolbar buttons on the Desktop
565 selection
.connect('changed', lambda s
: self
.play_or_download())
567 if gpodder
.ui
.diablo
:
568 # Set up the tap-and-hold context menu for podcasts
570 menu
.append(self
.itemUpdateChannel
.create_menu_item())
571 menu
.append(self
.itemEditChannel
.create_menu_item())
572 menu
.append(gtk
.SeparatorMenuItem())
573 menu
.append(self
.itemRemoveChannel
.create_menu_item())
574 menu
.append(gtk
.SeparatorMenuItem())
575 item
= gtk
.ImageMenuItem(_('Close this menu'))
576 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
580 menu
= self
.set_finger_friendly(menu
)
581 self
.treeChannels
.tap_and_hold_setup(menu
)
584 def init_download_list_treeview(self
):
585 # enable multiple selection support
586 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
587 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
589 # columns and renderers for "download progress" tab
590 # First column: [ICON] Episodename
591 column
= gtk
.TreeViewColumn(_('Episode'))
593 cell
= gtk
.CellRendererPixbuf()
595 cell
.set_fixed_size(50, 50)
596 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
597 column
.pack_start(cell
, expand
=False)
598 column
.add_attribute(cell
, 'stock-id', \
599 DownloadStatusModel
.C_ICON_NAME
)
601 cell
= gtk
.CellRendererText()
602 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
603 column
.pack_start(cell
, expand
=True)
604 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
605 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
606 column
.set_expand(True)
607 self
.treeDownloads
.append_column(column
)
609 # Second column: Progress
610 cell
= gtk
.CellRendererProgress()
611 cell
.set_property('yalign', .5)
612 cell
.set_property('ypad', 6)
613 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
614 value
=DownloadStatusModel
.C_PROGRESS
, \
615 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
616 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
617 column
.set_expand(False)
618 column
.set_property('min-width', 150)
619 column
.set_property('max-width', 150)
620 self
.treeDownloads
.append_column(column
)
622 self
.treeDownloads
.set_model(self
.download_status_model
)
623 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
625 def on_treeview_expose_event(self
, treeview
, event
):
626 if event
.window
== treeview
.get_bin_window():
627 model
= treeview
.get_model()
628 if (model
is not None and model
.get_iter_first() is not None):
631 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
632 ctx
= event
.window
.cairo_create()
633 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
634 event
.area
.width
, event
.area
.height
)
637 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
639 if role
== TreeViewHelper
.ROLE_EPISODES
:
640 if self
.currently_updating
:
641 text
= _('Loading episodes') + '...'
642 elif self
.config
.episode_list_view_mode
!= \
643 EpisodeListModel
.VIEW_ALL
:
644 text
= _('No episodes in current view')
646 text
= _('No episodes available')
647 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
648 if self
.config
.episode_list_view_mode
!= \
649 EpisodeListModel
.VIEW_ALL
and \
650 self
.config
.podcast_list_hide_boring
and \
651 len(self
.channels
) > 0:
652 text
= _('No podcasts in this view')
654 text
= _('No subscriptions')
655 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
656 text
= _('No active downloads')
658 raise Exception('on_treeview_expose_event: unknown role')
660 if gpodder
.ui
.fremantle
:
661 from gpodder
.gtkui
.frmntl
import style
662 font_desc
= style
.get_font_desc('LargeSystemFont')
666 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
)
670 def enable_download_list_update(self
):
671 if not self
.download_list_update_enabled
:
672 gobject
.timeout_add(1500, self
.update_downloads_list
)
673 self
.download_list_update_enabled
= True
675 def on_btnCleanUpDownloads_clicked(self
, button
):
676 model
= self
.download_status_model
678 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
679 changed_episode_urls
= []
680 for row_reference
, task
in all_tasks
:
681 if task
.status
in (task
.DONE
, task
.CANCELLED
, task
.FAILED
):
682 model
.remove(model
.get_iter(row_reference
.get_path()))
684 # We don't "see" this task anymore - remove it;
685 # this is needed, so update_episode_list_icons()
686 # below gets the correct list of "seen" tasks
687 self
.download_tasks_seen
.remove(task
)
688 except KeyError, key_error
:
689 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
690 changed_episode_urls
.append(task
.url
)
691 # Tell the task that it has been removed (so it can clean up)
692 task
.removed_from_list()
694 # Tell the podcasts tab to update icons for our removed podcasts
695 self
.update_episode_list_icons(changed_episode_urls
)
697 # Tell the shownotes window that we have removed the episode
698 if self
.episode_shownotes_window
is not None and \
699 self
.episode_shownotes_window
.episode
is not None and \
700 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
701 self
.episode_shownotes_window
._download
_status
_changed
(None)
703 # Update the tab title and downloads list
704 self
.update_downloads_list()
706 def on_tool_downloads_toggled(self
, toolbutton
):
707 if toolbutton
.get_active():
708 self
.wNotebook
.set_current_page(1)
710 self
.wNotebook
.set_current_page(0)
712 def update_downloads_list(self
):
714 model
= self
.download_status_model
716 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
717 total_speed
, total_size
, done_size
= 0, 0, 0
719 # Keep a list of all download tasks that we've seen
720 download_tasks_seen
= set()
722 # Remember the DownloadTask object for the episode that
723 # has been opened in the episode shownotes dialog (if any)
724 if self
.episode_shownotes_window
is not None:
725 shownotes_episode
= self
.episode_shownotes_window
.episode
726 shownotes_task
= None
728 shownotes_episode
= None
729 shownotes_task
= None
731 # Do not go through the list of the model is not (yet) available
736 self
.download_status_model
.request_update(row
.iter)
738 task
= row
[self
.download_status_model
.C_TASK
]
739 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
742 done_size
+= size
*progress
744 if shownotes_episode
is not None and \
745 shownotes_episode
.url
== task
.episode
.url
:
746 shownotes_task
= task
748 download_tasks_seen
.add(task
)
750 if status
== download
.DownloadTask
.DOWNLOADING
:
753 elif status
== download
.DownloadTask
.FAILED
:
755 elif status
== download
.DownloadTask
.DONE
:
757 elif status
== download
.DownloadTask
.QUEUED
:
759 elif status
== download
.DownloadTask
.PAUSED
:
764 # Remember which tasks we have seen after this run
765 self
.download_tasks_seen
= download_tasks_seen
767 if gpodder
.ui
.desktop
:
768 text
= [_('Downloads')]
769 if downloading
+ failed
+ finished
+ queued
> 0:
772 s
.append(_('%d active') % downloading
)
774 s
.append(_('%d failed') % failed
)
776 s
.append(_('%d done') % finished
)
778 s
.append(_('%d queued') % queued
)
779 text
.append(' (' + ', '.join(s
)+')')
780 self
.labelDownloads
.set_text(''.join(text
))
781 elif gpodder
.ui
.diablo
:
782 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
784 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
786 self
.tool_downloads
.set_label(_('Downloads'))
787 elif gpodder
.ui
.fremantle
:
788 if downloading
+ queued
> 0:
789 self
.button_downloads
.set_value(_('%d active') % (downloading
+queued
))
791 self
.button_downloads
.set_value(_('%d failed') % failed
)
793 self
.button_downloads
.set_value(_('%d paused') % paused
)
795 self
.button_downloads
.set_value(_('None active'))
797 title
= [self
.default_title
]
799 # We have to update all episodes/channels for which the status has
800 # changed. Accessing task.status_changed has the side effect of
801 # re-setting the changed flag, so we need to get the "changed" list
802 # of tuples first and split it into two lists afterwards
803 changed
= [(task
.url
, task
.podcast_url
) for task
in \
804 self
.download_tasks_seen
if task
.status_changed
]
805 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
806 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
808 count
= downloading
+ queued
811 title
.append( _('downloading one file'))
813 title
.append( _('downloading %d files') % count
)
816 percentage
= 100.0*done_size
/total_size
819 total_speed
= util
.format_filesize(total_speed
)
820 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
821 if self
.tray_icon
is not None:
822 # Update the tray icon status and progress bar
823 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
824 self
.tray_icon
.draw_progress_bar(percentage
/100.)
825 elif self
.last_download_count
> 0:
826 if self
.tray_icon
is not None:
827 # Update the tray icon status
828 self
.tray_icon
.set_status()
829 self
.tray_icon
.downloads_finished(self
.download_tasks_seen
)
830 if gpodder
.ui
.diablo
:
831 hildon
.hildon_banner_show_information(self
.gPodder
, None, 'gPodder: %s' % _('All downloads finished'))
832 log('All downloads have finished.', sender
=self
)
833 if self
.config
.cmd_all_downloads_complete
:
834 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
835 self
.last_download_count
= count
837 if not gpodder
.ui
.fremantle
:
838 self
.gPodder
.set_title(' - '.join(title
))
840 self
.update_episode_list_icons(episode_urls
)
841 if self
.episode_shownotes_window
is not None:
842 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
843 shownotes_task
!= self
.episode_shownotes_window
.task
:
844 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
845 self
.episode_shownotes_window
._download
_status
_progress
()
846 self
.play_or_download()
848 self
.update_podcast_list_model(channel_urls
)
850 if not self
.download_queue_manager
.are_queued_or_active_tasks():
851 self
.download_list_update_enabled
= False
853 return self
.download_list_update_enabled
855 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
856 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
857 # We return False here, so the update loop won't be called again,
858 # that's why we require the restart of gPodder in the message.
861 def on_config_changed(self
, name
, old_value
, new_value
):
862 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
863 self
.toolbar
.set_property('visible', new_value
)
864 elif name
== 'episode_list_descriptions':
865 self
.update_episode_list_model()
867 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
868 # With get_bin_window, we get the window that contains the rows without
869 # the header. The Y coordinate of this window will be the height of the
870 # treeview header. This is the amount we have to subtract from the
871 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
872 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
875 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
877 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
878 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
882 model
= treeview
.get_model()
883 iter = model
.get_iter(path
)
884 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
886 if role
== TreeViewHelper
.ROLE_EPISODES
:
887 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
888 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
889 id = model
.get_value(iter, PodcastListModel
.C_URL
)
891 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
892 if last_tooltip
is not None and last_tooltip
!= id:
893 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
895 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
897 if role
== TreeViewHelper
.ROLE_EPISODES
:
898 description
= model
.get_value(iter, EpisodeListModel
.C_DESCRIPTION_STRIPPED
)
899 if len(description
) > 400:
900 description
= description
[:398]+'[...]'
902 tooltip
.set_text(description
)
903 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
904 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
905 channel
.request_save_dir_size()
906 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
907 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
909 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
910 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
911 table
= gtk
.Table(rows
=3, columns
=3)
912 table
.set_row_spacings(5)
913 table
.set_col_spacings(5)
914 table
.set_border_width(5)
916 heading
= gtk
.Label()
917 heading
.set_alignment(0, 1)
918 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
919 table
.attach(heading
, 0, 1, 0, 1)
920 size_info
= gtk
.Label()
921 size_info
.set_alignment(1, 1)
922 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
923 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
924 table
.attach(size_info
, 2, 3, 0, 1)
926 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
928 if len(channel
.description
) < 500:
929 description
= channel
.description
931 pos
= channel
.description
.find('\n\n')
932 if pos
== -1 or pos
> 500:
933 description
= channel
.description
[:498]+'[...]'
935 description
= channel
.description
[:pos
]
937 description
= gtk
.Label(description
)
939 description
.set_markup(error_str
)
940 description
.set_alignment(0, 0)
941 description
.set_line_wrap(True)
942 table
.attach(description
, 0, 3, 2, 3)
945 tooltip
.set_custom(table
)
949 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
952 def treeview_allow_tooltips(self
, treeview
, allow
):
953 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
955 def update_m3u_playlist_clicked(self
, widget
):
956 if self
.active_channel
is not None:
957 self
.active_channel
.update_m3u_playlist()
958 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
960 def treeview_handle_context_menu_click(self
, treeview
, event
):
961 x
, y
= int(event
.x
), int(event
.y
)
962 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
964 selection
= treeview
.get_selection()
965 model
, paths
= selection
.get_selected_rows()
967 if path
is None or (path
not in paths
and \
968 event
.button
== self
.context_menu_mouse_button
):
969 # We have right-clicked, but not into the selection,
970 # assume we don't want to operate on the selection
973 if path
is not None and not paths
and \
974 event
.button
== self
.context_menu_mouse_button
:
975 # No selection or clicked outside selection;
976 # select the single item where we clicked
977 treeview
.grab_focus()
978 treeview
.set_cursor(path
, column
, 0)
982 # Unselect any remaining items (clicked elsewhere)
983 if hasattr(treeview
, 'is_rubber_banding_active'):
984 if not treeview
.is_rubber_banding_active():
985 selection
.unselect_all()
987 selection
.unselect_all()
991 def downloads_list_get_selection(self
, model
=None, paths
=None):
992 if model
is None and paths
is None:
993 selection
= self
.treeDownloads
.get_selection()
994 model
, paths
= selection
.get_selected_rows()
996 can_queue
, can_cancel
, can_pause
, can_remove
= (True,)*4
997 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
998 model
.get_value(model
.get_iter(path
), \
999 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1001 for row_reference
, task
in selected_tasks
:
1002 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1003 download
.DownloadTask
.FAILED
, \
1004 download
.DownloadTask
.CANCELLED
):
1006 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1007 download
.DownloadTask
.QUEUED
, \
1008 download
.DownloadTask
.DOWNLOADING
):
1010 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1011 download
.DownloadTask
.DOWNLOADING
):
1013 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1014 download
.DownloadTask
.FAILED
, \
1015 download
.DownloadTask
.DONE
):
1018 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
1020 def _for_each_task_set_status(self
, tasks
, status
):
1021 episode_urls
= set()
1022 model
= self
.treeDownloads
.get_model()
1023 for row_reference
, task
in tasks
:
1024 if status
== download
.DownloadTask
.QUEUED
:
1025 # Only queue task when its paused/failed/cancelled
1026 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
):
1027 self
.download_queue_manager
.add_task(task
)
1028 self
.enable_download_list_update()
1029 elif status
== download
.DownloadTask
.CANCELLED
:
1030 # Cancelling a download allowed when downloading/queued
1031 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1032 task
.status
= status
1033 # Cancelling paused downloads requires a call to .run()
1034 elif task
.status
== task
.PAUSED
:
1035 task
.status
= status
1036 # Call run, so the partial file gets deleted
1038 elif status
== download
.DownloadTask
.PAUSED
:
1039 # Pausing a download only when queued/downloading
1040 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1041 task
.status
= status
1042 elif status
is None:
1043 # Remove the selected task - cancel downloading/queued tasks
1044 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1045 task
.status
= task
.CANCELLED
1046 model
.remove(model
.get_iter(row_reference
.get_path()))
1047 # Remember the URL, so we can tell the UI to update
1049 # We don't "see" this task anymore - remove it;
1050 # this is needed, so update_episode_list_icons()
1051 # below gets the correct list of "seen" tasks
1052 self
.download_tasks_seen
.remove(task
)
1053 except KeyError, key_error
:
1054 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1055 episode_urls
.add(task
.url
)
1056 # Tell the task that it has been removed (so it can clean up)
1057 task
.removed_from_list()
1059 # We can (hopefully) simply set the task status here
1060 task
.status
= status
1061 # Tell the podcasts tab to update icons for our removed podcasts
1062 self
.update_episode_list_icons(episode_urls
)
1063 # Update the tab title and downloads list
1064 self
.update_downloads_list()
1066 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1067 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1069 if not hasattr(treeview
, 'is_rubber_banding_active'):
1072 return not treeview
.is_rubber_banding_active()
1074 if event
.button
== self
.context_menu_mouse_button
:
1075 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
= \
1076 self
.downloads_list_get_selection(model
, paths
)
1078 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
):
1079 # This creates a menu item for selection-wide actions
1080 item
= gtk
.ImageMenuItem(label
)
1081 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1082 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
))
1083 item
.set_sensitive(sensitive
)
1084 return self
.set_finger_friendly(item
)
1088 item
= gtk
.ImageMenuItem(_('Episode details'))
1089 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1090 if len(selected_tasks
) == 1:
1091 row_reference
, task
= selected_tasks
[0]
1092 episode
= task
.episode
1093 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1095 item
.set_sensitive(False)
1096 menu
.append(self
.set_finger_friendly(item
))
1097 menu
.append(gtk
.SeparatorMenuItem())
1098 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
))
1099 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1100 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1101 menu
.append(gtk
.SeparatorMenuItem())
1102 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1104 if gpodder
.ui
.maemo
:
1105 # Because we open the popup on left-click for Maemo,
1106 # we also include a non-action to close the menu
1107 menu
.append(gtk
.SeparatorMenuItem())
1108 item
= gtk
.ImageMenuItem(_('Close this menu'))
1109 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1111 menu
.append(self
.set_finger_friendly(item
))
1114 menu
.popup(None, None, None, event
.button
, event
.time
)
1117 def treeview_channels_show_context_menu(self
, treeview
, event
):
1118 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1122 if event
.button
== 3:
1127 item
= gtk
.ImageMenuItem( _('Open download folder'))
1128 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1129 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1132 item
= gtk
.ImageMenuItem( _('Update Feed'))
1133 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1134 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1135 item
.set_sensitive( not self
.updating_feed_cache
)
1138 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1139 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1140 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1143 if self
.active_channel
.link
:
1144 item
= gtk
.ImageMenuItem(_('Visit website'))
1145 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1146 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1149 if self
.active_channel
.channel_is_locked
:
1150 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1151 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1152 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1153 menu
.append(self
.set_finger_friendly(item
))
1155 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1156 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1157 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1158 menu
.append(self
.set_finger_friendly(item
))
1161 menu
.append( gtk
.SeparatorMenuItem())
1163 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1164 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1167 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1168 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1172 # Disable tooltips while we are showing the menu, so
1173 # the tooltip will not appear over the menu
1174 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1175 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1176 menu
.popup( None, None, None, event
.button
, event
.time
)
1180 def on_itemClose_activate(self
, widget
):
1181 if self
.tray_icon
is not None:
1182 self
.iconify_main_window()
1184 self
.on_gPodder_delete_event(widget
)
1186 def cover_file_removed(self
, channel_url
):
1188 The Cover Downloader calls this when a previously-
1189 available cover has been removed from the disk. We
1190 have to update our model to reflect this change.
1192 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1194 def cover_download_finished(self
, channel_url
, pixbuf
):
1196 The Cover Downloader calls this when it has finished
1197 downloading (or registering, if already downloaded)
1198 a new channel cover, which is ready for displaying.
1200 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1202 def save_episode_as_file(self
, episode
):
1203 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1204 if episode
.was_downloaded(and_exists
=True):
1205 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1206 copy_from
= episode
.local_filename(create
=False)
1207 assert copy_from
is not None
1208 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1209 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1210 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1212 def copy_episodes_bluetooth(self
, episodes
):
1213 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1215 def convert_and_send_thread(episode
):
1216 for episode
in episodes
:
1217 filename
= episode
.local_filename(create
=False)
1218 assert filename
is not None
1219 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1220 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1221 (base
, ext
) = os
.path
.splitext(filename
)
1222 if not destfile
.endswith(ext
):
1226 shutil
.copyfile(filename
, destfile
)
1227 util
.bluetooth_send_file(destfile
)
1229 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1230 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1232 util
.delete_file(destfile
)
1234 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1236 def get_device_name(self
):
1237 if self
.config
.device_type
== 'ipod':
1239 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1240 return _('MP3 player')
1242 return '(unknown device)'
1244 def _treeview_button_released(self
, treeview
, event
):
1245 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1246 dy
= int(abs(event
.y
-ypos
))
1247 dx
= int(event
.x
-xpos
)
1249 selection
= treeview
.get_selection()
1250 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1251 if path
is None or dy
> 30:
1252 return (False, dx
, dy
)
1254 path
, column
, x
, y
= path
1255 selection
.select_path(path
)
1256 treeview
.set_cursor(path
)
1257 treeview
.grab_focus()
1259 return (True, dx
, dy
)
1261 def treeview_channels_handle_gestures(self
, treeview
, event
):
1262 if self
.currently_updating
:
1265 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1268 if self
.config
.maemo_enable_gestures
:
1270 self
.on_itemUpdateChannel_activate()
1272 self
.on_itemEditChannel_activate(treeview
)
1276 def treeview_available_handle_gestures(self
, treeview
, event
):
1277 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1280 if self
.config
.maemo_enable_gestures
:
1282 self
.on_playback_selected_episodes(None)
1285 self
.on_shownotes_selected_episodes(None)
1288 # Pass the event to the context menu handler for treeAvailable
1289 self
.treeview_available_show_context_menu(treeview
, event
)
1293 def treeview_available_show_context_menu(self
, treeview
, event
):
1294 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1296 if not hasattr(treeview
, 'is_rubber_banding_active'):
1299 return not treeview
.is_rubber_banding_active()
1301 if event
.button
== self
.context_menu_mouse_button
:
1302 episodes
= self
.get_selected_episodes()
1303 any_locked
= any(e
.is_locked
for e
in episodes
)
1304 any_played
= any(e
.is_played
for e
in episodes
)
1305 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1309 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1311 if open_instead_of_play
:
1312 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1314 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1316 item
.set_sensitive(can_play
)
1317 item
.connect('activate', self
.on_playback_selected_episodes
)
1318 menu
.append(self
.set_finger_friendly(item
))
1321 item
= gtk
.ImageMenuItem(_('Download'))
1322 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1323 item
.set_sensitive(can_download
)
1324 item
.connect('activate', self
.on_download_selected_episodes
)
1325 menu
.append(self
.set_finger_friendly(item
))
1327 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1328 item
.connect('activate', self
.on_item_cancel_download_activate
)
1329 menu
.append(self
.set_finger_friendly(item
))
1331 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1332 item
.set_sensitive(can_delete
)
1333 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1334 menu
.append(self
.set_finger_friendly(item
))
1337 item
= gtk
.ImageMenuItem(_('Do not download'))
1338 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1339 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1340 menu
.append(self
.set_finger_friendly(item
))
1342 item
= gtk
.ImageMenuItem(_('Mark as new'))
1343 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
1344 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1345 menu
.append(self
.set_finger_friendly(item
))
1349 # Ok, this probably makes sense to only display for downloaded files
1350 if can_play
and not can_download
:
1351 menu
.append( gtk
.SeparatorMenuItem())
1352 item
= gtk
.ImageMenuItem(_('Save to disk'))
1353 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
1354 item
.connect('activate', lambda w
: [self
.save_episode_as_file(e
) for e
in episodes
])
1355 menu
.append(self
.set_finger_friendly(item
))
1356 if self
.bluetooth_available
:
1357 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
1358 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1359 item
.connect('activate', lambda w
: self
.copy_episodes_bluetooth(episodes
))
1360 menu
.append(self
.set_finger_friendly(item
))
1362 item
= gtk
.ImageMenuItem(_('Transfer to %s') % self
.get_device_name())
1363 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1364 item
.connect('activate', lambda w
: self
.on_sync_to_ipod_activate(w
, episodes
))
1365 menu
.append(self
.set_finger_friendly(item
))
1368 menu
.append( gtk
.SeparatorMenuItem())
1370 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
1371 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
1372 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
1373 menu
.append(self
.set_finger_friendly(item
))
1375 item
= gtk
.ImageMenuItem(_('Mark as played'))
1376 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
1377 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
1378 menu
.append(self
.set_finger_friendly(item
))
1381 item
= gtk
.ImageMenuItem(_('Allow deletion'))
1382 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1383 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, False))
1384 menu
.append(self
.set_finger_friendly(item
))
1386 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
1387 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1388 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, True))
1389 menu
.append(self
.set_finger_friendly(item
))
1391 menu
.append(gtk
.SeparatorMenuItem())
1392 # Single item, add episode information menu item
1393 item
= gtk
.ImageMenuItem(_('Episode details'))
1394 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1395 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1396 menu
.append(self
.set_finger_friendly(item
))
1398 # If we have it, also add episode website link
1399 if episodes
[0].link
and episodes
[0].link
!= episodes
[0].url
:
1400 item
= gtk
.ImageMenuItem(_('Visit website'))
1401 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1402 item
.connect('activate', lambda w
: util
.open_website(episodes
[0].link
))
1403 menu
.append(self
.set_finger_friendly(item
))
1405 if gpodder
.ui
.maemo
:
1406 # Because we open the popup on left-click for Maemo,
1407 # we also include a non-action to close the menu
1408 menu
.append(gtk
.SeparatorMenuItem())
1409 item
= gtk
.ImageMenuItem(_('Close this menu'))
1410 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1411 menu
.append(self
.set_finger_friendly(item
))
1414 # Disable tooltips while we are showing the menu, so
1415 # the tooltip will not appear over the menu
1416 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1417 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1418 menu
.popup( None, None, None, event
.button
, event
.time
)
1422 def set_title(self
, new_title
):
1423 if not gpodder
.ui
.fremantle
:
1424 self
.default_title
= new_title
1425 self
.gPodder
.set_title(new_title
)
1427 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1429 Updates the status icons in the episode list.
1431 If urls is given, it should be a list of URLs
1432 of episodes that should be updated.
1434 If urls is None, set ONE OF selected, all to
1435 True (the former updates just the selected
1436 episodes and the latter updates all episodes).
1438 if urls
is not None:
1439 # We have a list of URLs to walk through
1440 self
.episode_list_model
.update_by_urls(urls
, \
1441 self
.episode_is_downloading
, \
1442 self
.config
.episode_list_descriptions
and \
1444 elif selected
and not all
:
1445 # We should update all selected episodes
1446 selection
= self
.treeAvailable
.get_selection()
1447 model
, paths
= selection
.get_selected_rows()
1448 for path
in reversed(paths
):
1449 iter = model
.get_iter(path
)
1450 self
.episode_list_model
.update_by_filter_iter(iter, \
1451 self
.episode_is_downloading
, \
1452 self
.config
.episode_list_descriptions
and \
1454 elif all
and not selected
:
1455 # We update all (even the filter-hidden) episodes
1456 self
.episode_list_model
.update_all(\
1457 self
.episode_is_downloading
, \
1458 self
.config
.episode_list_descriptions
and \
1461 # Wrong/invalid call - have to specify at least one parameter
1462 raise ValueError('Invalid call to update_episode_list_icons')
1464 def episode_list_status_changed(self
, episodes
):
1465 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1466 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1469 def clean_up_downloads(self
, delete_partial
=False):
1470 # Clean up temporary files left behind by old gPodder versions
1471 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
1474 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
1476 for tempfile
in temporary_files
:
1477 util
.delete_file(tempfile
)
1479 # Clean up empty download folders and abandoned download folders
1480 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
1481 for ddir
in download_dirs
:
1482 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1483 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
1484 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
1485 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
1486 shutil
.rmtree(ddir
, ignore_errors
=True)
1488 def streaming_possible(self
):
1489 return self
.config
.player
and \
1490 self
.config
.player
!= 'default' and \
1493 def playback_episodes_for_real(self
, episodes
):
1494 groups
= collections
.defaultdict(list)
1495 for episode
in episodes
:
1496 file_type
= episode
.file_type()
1497 if file_type
== 'video' and self
.config
.videoplayer
and \
1498 self
.config
.videoplayer
!= 'default':
1499 player
= self
.config
.videoplayer
1500 if gpodder
.ui
.diablo
:
1501 # Use the wrapper script if it's installed to crop 3GP YouTube
1502 # videos to fit the screen (looks much nicer than w/ black border)
1503 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
1504 player
= 'gpodder-mplayer'
1505 elif file_type
== 'audio' and self
.config
.player
and \
1506 self
.config
.player
!= 'default':
1507 player
= self
.config
.player
1511 if file_type
not in ('audio', 'video') or \
1512 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
1513 (file_type
== 'video' and not self
.config
.video_played_dbus
):
1514 # Mark episode as played in the database
1515 episode
.mark(is_played
=True)
1517 filename
= episode
.local_filename(create
=False)
1518 if filename
is None or not os
.path
.exists(filename
):
1519 filename
= episode
.url
1520 groups
[player
].append(filename
)
1522 # Open episodes with system default player
1523 if 'default' in groups
:
1524 for filename
in groups
['default']:
1525 log('Opening with system default: %s', filename
, sender
=self
)
1526 util
.gui_open(filename
)
1527 del groups
['default']
1528 elif gpodder
.ui
.maemo
:
1529 # When on Maemo and not opening with default, show a notification
1530 # (no startup notification for Panucci / MPlayer yet...)
1531 if len(episodes
) == 1:
1532 text
= _('Opening %s') % episodes
[0].title
1534 text
= _('Opening %d episodes') % len(episodes
)
1536 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, text
)
1538 def destroy_banner_later(banner
):
1541 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
1543 # For each type now, go and create play commands
1544 for group
in groups
:
1545 for command
in util
.format_desktop_command(group
, groups
[group
]):
1546 log('Executing: %s', repr(command
), sender
=self
)
1547 subprocess
.Popen(command
)
1549 def playback_episodes(self
, episodes
):
1550 episodes
= [e
for e
in episodes
if \
1551 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()]
1554 self
.playback_episodes_for_real(episodes
)
1555 except Exception, e
:
1556 log('Error in playback!', sender
=self
, traceback
=True)
1557 self
.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget
=self
.toolPreferences
)
1559 channel_urls
= set()
1560 episode_urls
= set()
1561 for episode
in episodes
:
1562 channel_urls
.add(episode
.channel
.url
)
1563 episode_urls
.add(episode
.url
)
1564 self
.update_episode_list_icons(episode_urls
)
1565 self
.update_podcast_list_model(channel_urls
)
1567 def play_or_download(self
):
1568 if not gpodder
.ui
.fremantle
:
1569 if self
.wNotebook
.get_current_page() > 0:
1570 if gpodder
.ui
.desktop
:
1571 self
.toolCancel
.set_sensitive(True)
1574 if self
.currently_updating
:
1575 return (False, False, False, False, False, False)
1577 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1578 ( is_played
, is_locked
) = (False,)*2
1580 open_instead_of_play
= False
1582 selection
= self
.treeAvailable
.get_selection()
1583 if selection
.count_selected_rows() > 0:
1584 (model
, paths
) = selection
.get_selected_rows()
1587 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
1589 if episode
.file_type() not in ('audio', 'video'):
1590 open_instead_of_play
= True
1592 if episode
.was_downloaded():
1593 can_play
= episode
.was_downloaded(and_exists
=True)
1595 is_played
= episode
.is_played
1596 is_locked
= episode
.is_locked
1600 if self
.episode_is_downloading(episode
):
1605 can_download
= can_download
and not can_cancel
1606 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
1607 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
1609 if gpodder
.ui
.desktop
:
1610 if open_instead_of_play
:
1611 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
1613 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
1614 self
.toolPlay
.set_sensitive( can_play
)
1615 self
.toolDownload
.set_sensitive( can_download
)
1616 self
.toolTransfer
.set_sensitive( can_transfer
)
1617 self
.toolCancel
.set_sensitive( can_cancel
)
1619 if not gpodder
.ui
.fremantle
:
1620 self
.item_cancel_download
.set_sensitive(can_cancel
)
1621 self
.itemDownloadSelected
.set_sensitive(can_download
)
1622 self
.itemOpenSelected
.set_sensitive(can_play
)
1623 self
.itemPlaySelected
.set_sensitive(can_play
)
1624 self
.itemDeleteSelected
.set_sensitive(can_play
and not can_download
)
1625 self
.item_toggle_played
.set_sensitive(can_play
)
1626 self
.item_toggle_lock
.set_sensitive(can_play
)
1627 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
1628 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
1630 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
1632 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1633 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1635 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1636 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1638 def episode_new_status_changed(self
, urls
):
1639 self
.update_podcast_list_model()
1640 self
.update_episode_list_icons(urls
)
1642 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
1643 """Update the podcast list treeview model
1645 If urls is given, it should list the URLs of each
1646 podcast that has to be updated in the list.
1648 If selected is True, only update the model contents
1649 for the currently-selected podcast - nothing more.
1651 The caller can optionally specify "select_url",
1652 which is the URL of the podcast that is to be
1653 selected in the list after the update is complete.
1654 This only works if the podcast list has to be
1655 reloaded; i.e. something has been added or removed
1656 since the last update of the podcast list).
1658 selection
= self
.treeChannels
.get_selection()
1659 model
, iter = selection
.get_selected()
1662 # very cheap! only update selected channel
1663 if iter is not None:
1664 self
.podcast_list_model
.update_by_filter_iter(iter)
1665 elif not self
.channel_list_changed
:
1666 # we can keep the model, but have to update some
1668 # still cheaper than reloading the whole list
1669 self
.podcast_list_model
.update_all()
1671 # ok, we got a bunch of urls to update
1672 self
.podcast_list_model
.update_by_urls(urls
)
1674 if model
and iter and select_url
is None:
1675 # Get the URL of the currently-selected podcast
1676 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
1678 # Update the podcast list model with new channels
1679 self
.podcast_list_model
.set_channels(self
.channels
)
1682 selected_iter
= model
.get_iter_first()
1683 # Find the previously-selected URL in the new
1684 # model if we have an URL (else select first)
1685 if select_url
is not None:
1686 pos
= model
.get_iter_first()
1687 while pos
is not None:
1688 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
1689 if url
== select_url
:
1692 pos
= model
.iter_next(pos
)
1694 if not gpodder
.ui
.fremantle
:
1695 if selected_iter
is not None:
1696 selection
.select_iter(selected_iter
)
1697 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1699 log('Cannot select podcast in list', traceback
=True, sender
=self
)
1700 self
.channel_list_changed
= False
1702 def episode_is_downloading(self
, episode
):
1703 """Returns True if the given episode is being downloaded at the moment"""
1707 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
1709 def update_episode_list_model(self
):
1710 if self
.channels
and self
.active_channel
is not None:
1711 if gpodder
.ui
.diablo
:
1712 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, _('Loading episodes'))
1716 if gpodder
.ui
.fremantle
:
1717 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
1719 self
.currently_updating
= True
1720 self
.episode_list_model
.clear()
1721 def do_update_episode_list_model():
1722 self
.episode_list_model
.add_from_channel(\
1723 self
.active_channel
, \
1724 self
.episode_is_downloading
, \
1725 self
.config
.episode_list_descriptions \
1726 and gpodder
.ui
.desktop
)
1728 def on_episode_list_model_updated():
1729 if banner
is not None:
1731 if gpodder
.ui
.fremantle
:
1732 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
1733 self
.treeAvailable
.columns_autosize()
1734 self
.currently_updating
= False
1735 self
.play_or_download()
1736 util
.idle_add(on_episode_list_model_updated
)
1737 threading
.Thread(target
=do_update_episode_list_model
).start()
1739 self
.episode_list_model
.clear()
1741 def offer_new_episodes(self
, channels
=None):
1742 new_episodes
= self
.get_new_episodes(channels
)
1744 self
.new_episodes_show(new_episodes
)
1748 def add_podcast_list(self
, urls
, auth_tokens
=None):
1749 """Subscribe to a list of podcast given their URLs
1751 If auth_tokens is given, it should be a dictionary
1752 mapping URLs to (username, password) tuples."""
1754 if auth_tokens
is None:
1757 # Sort and split the URL list into five buckets
1758 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
1759 for input_url
in urls
:
1760 url
= util
.normalize_feed_url(input_url
)
1762 # Fail this one because the URL is not valid
1763 failed
.append(input_url
)
1764 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
1765 # A podcast already exists in the list for this URL
1766 existing
.append(url
)
1768 # This URL has survived the first round - queue for add
1770 if url
!= input_url
and input_url
in auth_tokens
:
1771 auth_tokens
[url
] = auth_tokens
[input_url
]
1776 progress
= ProgressIndicator(_('Adding podcasts'), \
1777 _('Please wait while episode information is downloaded.'), \
1778 parent
=self
.main_window
)
1780 def on_after_update():
1781 progress
.on_finished()
1782 # Report already-existing subscriptions to the user
1784 title
= _('Existing subscriptions skipped')
1785 message
= _('You are already subscribed to these podcasts:') \
1786 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
1787 self
.show_message(message
, title
, widget
=self
.treeChannels
)
1789 # Report subscriptions that require authentication
1793 title
= _('Podcast requires authentication')
1794 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
1795 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
1797 retry_podcasts
[url
] = auth_tokens
1799 # Stop asking the user for more login data
1802 error_messages
[url
] = _('Authentication failed')
1806 # If we have authentication data to retry, do so here
1808 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
1810 # Report website redirections
1811 for url
in redirections
:
1812 title
= _('Website redirection detected')
1813 message
= _('The URL %s redirects to %s.') \
1814 + '\n\n' + _('Do you want to visit the website now?')
1815 message
= message
% (url
, redirections
[url
])
1816 if self
.show_confirmation(message
, title
):
1817 util
.open_website(url
)
1821 # Report failed subscriptions to the user
1823 title
= _('Could not add some podcasts')
1824 message
= _('Some podcasts could not be added to your list:') \
1825 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
1826 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
1827 self
.show_message(message
, title
, important
=True)
1829 # If at least one podcast has been added, save and update all
1830 if self
.channel_list_changed
:
1831 self
.save_channels_opml()
1833 # If only one podcast was added, select it after the update
1834 if len(worked
) == 1:
1839 # Update the list of subscribed podcasts
1840 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
1841 self
.update_podcasts_tab()
1843 # Offer to download new episodes
1844 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
1847 # After the initial sorting and splitting, try all queued podcasts
1848 length
= len(queued
)
1849 for index
, url
in enumerate(queued
):
1850 progress
.on_progress(float(index
)/float(length
))
1851 progress
.on_message(url
)
1852 log('QUEUE RUNNER: %s', url
, sender
=self
)
1854 # The URL is valid and does not exist already - subscribe!
1855 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
1856 authentication_tokens
=auth_tokens
.get(url
, None), \
1857 max_episodes
=self
.config
.max_episodes_per_feed
, \
1858 download_dir
=self
.config
.download_dir
)
1861 username
, password
= util
.username_password_from_url(url
)
1862 except ValueError, ve
:
1863 username
, password
= (None, None)
1865 if username
is not None and channel
.username
is None and \
1866 password
is not None and channel
.password
is None:
1867 channel
.username
= username
1868 channel
.password
= password
1871 self
._update
_cover
(channel
)
1872 except feedcore
.AuthenticationRequired
:
1873 if url
in auth_tokens
:
1874 # Fail for wrong authentication data
1875 error_messages
[url
] = _('Authentication failed')
1878 # Queue for login dialog later
1881 except feedcore
.WifiLogin
, error
:
1882 redirections
[url
] = error
.data
1884 error_messages
[url
] = _('Redirection detected')
1886 except Exception, e
:
1887 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
1888 error_messages
[url
] = str(e
)
1892 assert channel
is not None
1893 worked
.append(channel
.url
)
1894 self
.channels
.append(channel
)
1895 self
.channel_list_changed
= True
1896 util
.idle_add(on_after_update
)
1897 threading
.Thread(target
=thread_proc
).start()
1899 def save_channels_opml(self
):
1900 exporter
= opml
.Exporter(gpodder
.subscription_file
)
1901 return exporter
.write(self
.channels
)
1903 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
1905 self
.updating_feed_cache
= False
1907 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
1908 self
.channel_list_changed
= True
1909 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
1911 # Only search for new episodes in podcasts that have been
1912 # updated, not in other podcasts (for single-feed updates)
1913 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
1915 if gpodder
.ui
.fremantle
:
1916 if self
._fremantle
_update
_banner
is not None:
1917 self
._fremantle
_update
_banner
.destroy()
1918 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1919 self
.update_podcasts_tab()
1921 self
.new_episodes_show(episodes
)
1923 self
.show_message(_('No new episodes. Please check for new episodes later.'), important
=True)
1927 self
.tray_icon
.set_status()
1929 if self
.feed_cache_update_cancelled
:
1930 # The user decided to abort the feed update
1931 self
.show_update_feeds_buttons()
1933 # Nothing new here - but inform the user
1934 self
.pbFeedUpdate
.set_fraction(1.0)
1935 self
.pbFeedUpdate
.set_text(_('No new episodes'))
1936 self
.feed_cache_update_cancelled
= True
1937 self
.btnCancelFeedUpdate
.show()
1938 self
.btnCancelFeedUpdate
.set_sensitive(True)
1939 if gpodder
.ui
.maemo
:
1940 # btnCancelFeedUpdate is a ToolButton on Maemo
1941 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
1943 # btnCancelFeedUpdate is a normal gtk.Button
1944 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
1946 # New episodes are available
1947 self
.pbFeedUpdate
.set_fraction(1.0)
1948 # Are we minimized and should we auto download?
1949 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
1950 self
.download_episode_list(episodes
)
1951 if len(episodes
) == 1:
1952 title
= _('Downloading one new episode.')
1954 title
= _('Downloading %d new episodes.') % len(episodes
)
1956 if not gpodder
.ui
.fremantle
:
1957 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
1958 self
.show_update_feeds_buttons()
1960 self
.show_update_feeds_buttons()
1961 # New episodes are available and we are not minimized
1962 if not self
.config
.do_not_show_new_episodes_dialog
:
1963 self
.new_episodes_show(episodes
, notification
=True)
1965 if len(episodes
) == 1:
1966 message
= _('One new episode is available for download')
1968 message
= _('%i new episodes are available for download' % len(episodes
))
1970 self
.pbFeedUpdate
.set_text(message
)
1972 def _update_cover(self
, channel
):
1973 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
1974 self
.cover_downloader
.request_cover(channel
)
1976 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
1977 total
= len(channels
)
1979 for updated
, channel
in enumerate(channels
):
1980 if not self
.feed_cache_update_cancelled
:
1982 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1983 if channel
.query_automatic_update() or total
== 1 or not self
.config
.feed_update_skipping
:
1984 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
1986 log('Skipping update of %s (see feed_update_skipping)', channel
.title
, sender
=self
)
1987 self
._update
_cover
(channel
)
1988 except Exception, e
:
1989 self
.notification(_('There has been an error updating %s: %s') % (saxutils
.escape(channel
.url
), saxutils
.escape(str(e
))), _('Error while updating feed'), widget
=self
.treeChannels
)
1990 log('Error: %s', str(e
), sender
=self
, traceback
=True)
1992 if self
.feed_cache_update_cancelled
:
1995 if gpodder
.ui
.fremantle
:
1996 self
.button_podcasts
.set_value(_('%d/%d updated') % (updated
, total
))
1999 # By the time we get here the update may have already been cancelled
2000 if not self
.feed_cache_update_cancelled
:
2001 def update_progress():
2002 progression
= _('Updated %s (%d/%d)') % (channel
.title
, updated
, total
)
2003 self
.pbFeedUpdate
.set_text(progression
)
2005 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2006 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2007 util
.idle_add(update_progress
)
2009 updated_urls
= [c
.url
for c
in channels
]
2010 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2012 def show_update_feeds_buttons(self
):
2013 # Make sure that the buttons for updating feeds
2014 # appear - this should happen after a feed update
2015 if gpodder
.ui
.maemo
:
2016 self
.btnUpdateSelectedFeed
.show()
2017 self
.toolFeedUpdateProgress
.hide()
2018 self
.btnCancelFeedUpdate
.hide()
2019 self
.btnCancelFeedUpdate
.set_is_important(False)
2020 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2021 self
.toolbarSpacer
.set_expand(True)
2022 self
.toolbarSpacer
.set_draw(False)
2024 self
.hboxUpdateFeeds
.hide()
2025 self
.btnUpdateFeeds
.show()
2026 self
.itemUpdate
.set_sensitive(True)
2027 self
.itemUpdateChannel
.set_sensitive(True)
2029 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2030 if not self
.feed_cache_update_cancelled
:
2031 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2032 self
.feed_cache_update_cancelled
= True
2033 self
.btnCancelFeedUpdate
.set_sensitive(False)
2035 self
.show_update_feeds_buttons()
2037 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2038 if self
.updating_feed_cache
:
2041 if not force_update
:
2042 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2043 self
.channel_list_changed
= True
2044 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2047 self
.updating_feed_cache
= True
2049 if channels
is None:
2050 channels
= self
.channels
2052 if gpodder
.ui
.fremantle
:
2053 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2054 self
._fremantle
_update
_banner
= hildon
.hildon_banner_show_animation(self
.main_window
, \
2055 '', _('Updating podcast feeds'))
2057 self
.itemUpdate
.set_sensitive(False)
2058 self
.itemUpdateChannel
.set_sensitive(False)
2061 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2063 if len(channels
) == 1:
2064 text
= _('Updating "%s"...') % channels
[0].title
2066 text
= _('Updating %d feeds...') % len(channels
)
2067 self
.pbFeedUpdate
.set_text(text
)
2068 self
.pbFeedUpdate
.set_fraction(0)
2070 self
.feed_cache_update_cancelled
= False
2071 self
.btnCancelFeedUpdate
.show()
2072 self
.btnCancelFeedUpdate
.set_sensitive(True)
2073 if gpodder
.ui
.maemo
:
2074 self
.toolbarSpacer
.set_expand(False)
2075 self
.toolbarSpacer
.set_draw(True)
2076 self
.btnUpdateSelectedFeed
.hide()
2077 self
.toolFeedUpdateProgress
.show_all()
2079 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2080 self
.hboxUpdateFeeds
.show_all()
2081 self
.btnUpdateFeeds
.hide()
2083 args
= (channels
, select_url_afterwards
)
2084 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2086 def on_gPodder_delete_event(self
, widget
, *args
):
2087 """Called when the GUI wants to close the window
2088 Displays a confirmation dialog (and closes/hides gPodder)
2091 downloading
= self
.download_status_model
.are_downloads_in_progress()
2093 # Only iconify if we are using the window's "X" button,
2094 # but not when we are using "Quit" in the menu or toolbar
2095 if not self
.config
.on_quit_ask
and self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2096 self
.iconify_main_window()
2097 elif self
.config
.on_quit_ask
or downloading
:
2098 if gpodder
.ui
.maemo
:
2099 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2101 self
.close_gpodder()
2104 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2105 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2106 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2108 title
= _('Quit gPodder')
2110 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2112 message
= _('Do you really want to quit gPodder now?')
2114 dialog
.set_title(title
)
2115 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2117 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2118 dialog
.vbox
.pack_start(cb_ask
)
2121 quit_button
.grab_focus()
2122 result
= dialog
.run()
2125 if result
== gtk
.RESPONSE_CLOSE
:
2126 if not downloading
and cb_ask
.get_active() == True:
2127 self
.config
.on_quit_ask
= False
2128 self
.close_gpodder()
2130 self
.close_gpodder()
2134 def close_gpodder(self
):
2135 """ clean everything and exit properly
2138 if self
.save_channels_opml():
2139 if self
.config
.my_gpodder_autoupload
:
2140 log('Uploading to my.gpodder.org on close', sender
=self
)
2141 util
.idle_add(self
.on_upload_to_mygpo
, None)
2143 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2147 if self
.tray_icon
is not None:
2148 self
.tray_icon
.set_visible(False)
2150 # Notify all tasks to to carry out any clean-up actions
2151 self
.download_status_model
.tell_all_tasks_to_quit()
2153 while gtk
.events_pending():
2154 gtk
.main_iteration(False)
2161 def get_old_episodes(self
):
2163 for channel
in self
.channels
:
2164 for episode
in channel
.get_downloaded_episodes():
2165 if episode
.age_in_days() > self
.config
.episode_old_age
and \
2166 not episode
.is_locked
and episode
.is_played
:
2167 episodes
.append(episode
)
2170 def delete_episode_list(self
, episodes
, confirm
=True):
2174 count
= len(episodes
)
2177 episode
= episodes
[0]
2178 if episode
.is_locked
:
2179 title
= _('%s is locked') % saxutils
.escape(episode
.title
)
2180 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2181 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2184 title
= _('Remove %s?') % saxutils
.escape(episode
.title
)
2185 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.")
2187 title
= _('Remove %d episodes?') % count
2188 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.')
2190 locked_count
= sum(int(e
.is_locked
) for e
in episodes
if e
.is_locked
is not None)
2192 if count
== locked_count
:
2193 title
= _('Episodes are locked')
2194 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2195 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2197 elif locked_count
> 0:
2198 title
= _('Remove %d out of %d episodes?') % (count
-locked_count
, count
)
2199 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.')
2201 if confirm
and not self
.show_confirmation(message
, title
):
2204 episode_urls
= set()
2205 channel_urls
= set()
2206 for episode
in episodes
:
2207 if episode
.is_locked
:
2208 log('Not deleting episode (is locked): %s', episode
.title
)
2210 log('Deleting episode: %s', episode
.title
)
2211 episode
.delete_from_disk()
2212 episode_urls
.add(episode
.url
)
2213 channel_urls
.add(episode
.channel
.url
)
2215 # Tell the shownotes window that we have removed the episode
2216 if self
.episode_shownotes_window
is not None and \
2217 self
.episode_shownotes_window
.episode
is not None and \
2218 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2219 self
.episode_shownotes_window
._download
_status
_changed
(None)
2221 # Episodes have been deleted - persist the database
2224 self
.update_episode_list_icons(episode_urls
)
2225 self
.update_podcast_list_model(channel_urls
)
2226 self
.play_or_download()
2229 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2230 if gpodder
.ui
.maemo
:
2232 ('maemo_remove_markup', None, None, _('Episode')),
2236 ('title_markup', None, None, _('Episode')),
2237 ('channel_prop', None, None, _('Podcast')),
2238 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2239 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2240 ('played_prop', None, None, _('Status')),
2241 ('age_prop', None, None, _('Downloaded')),
2244 selection_buttons
= {
2245 _('Select played'): lambda episode
: episode
.is_played
,
2246 _('Select older than %d days') % self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2249 instructions
= _('Select the episodes you want to delete:')
2253 for channel
in self
.channels
:
2254 for episode
in channel
.get_downloaded_episodes():
2255 if not episode
.is_locked
:
2256 episodes
.append(episode
)
2257 selected
.append(episode
.is_played
)
2259 gPodderEpisodeSelector(self
.gPodder
, title
= _('Remove old episodes'), instructions
= instructions
, \
2260 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2261 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2262 selection_buttons
= selection_buttons
, _config
=self
.config
)
2264 def on_selected_episodes_status_changed(self
):
2265 self
.update_episode_list_icons(selected
=True)
2266 self
.update_podcast_list_model(selected
=True)
2269 def mark_selected_episodes_new(self
):
2270 for episode
in self
.get_selected_episodes():
2272 self
.on_selected_episodes_status_changed()
2274 def mark_selected_episodes_old(self
):
2275 for episode
in self
.get_selected_episodes():
2277 self
.on_selected_episodes_status_changed()
2279 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2280 for episode
in self
.get_selected_episodes():
2282 episode
.mark(is_played
=not episode
.is_played
)
2284 episode
.mark(is_played
=new_value
)
2285 self
.on_selected_episodes_status_changed()
2287 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2288 for episode
in self
.get_selected_episodes():
2290 episode
.mark(is_locked
=not episode
.is_locked
)
2292 episode
.mark(is_locked
=new_value
)
2293 self
.on_selected_episodes_status_changed()
2295 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2296 if self
.active_channel
is None:
2299 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2300 self
.active_channel
.update_channel_lock()
2302 for episode
in self
.active_channel
.get_all_episodes():
2303 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
2305 self
.update_podcast_list_model(selected
=True)
2306 self
.update_episode_list_icons(all
=True)
2308 def on_itemUpdateChannel_activate(self
, widget
=None):
2309 if self
.active_channel
is None:
2310 title
= _('No podcast selected')
2311 message
= _('Please select a podcast in the podcasts list to update.')
2312 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2315 self
.update_feed_cache(channels
=[self
.active_channel
])
2317 def on_itemUpdate_activate(self
, widget
=None):
2319 self
.update_feed_cache()
2321 gPodderWelcome(self
.gPodder
, center_on_widget
=self
.gPodder
, show_example_podcasts_callback
=self
.on_itemImportChannels_activate
, setup_my_gpodder_callback
=self
.on_download_from_mygpo
)
2323 def download_episode_list_paused(self
, episodes
):
2324 self
.download_episode_list(episodes
, True)
2326 def download_episode_list(self
, episodes
, add_paused
=False):
2327 for episode
in episodes
:
2328 log('Downloading episode: %s', episode
.title
, sender
= self
)
2329 if not episode
.was_downloaded(and_exists
=True):
2331 for task
in self
.download_tasks_seen
:
2332 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
2333 self
.download_queue_manager
.add_task(task
)
2334 self
.enable_download_list_update()
2342 task
= download
.DownloadTask(episode
, self
.config
)
2343 except Exception, e
:
2344 self
.show_message(_('Download error while downloading %s:\n\n%s') % (episode
.title
, str(e
)), _('Download error'), important
=True)
2345 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
2349 task
.status
= task
.PAUSED
2351 self
.download_queue_manager
.add_task(task
)
2353 self
.download_status_model
.register_task(task
)
2354 self
.enable_download_list_update()
2356 def cancel_task_list(self
, tasks
):
2361 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
2362 task
.status
= task
.CANCELLED
2363 elif task
.status
== task
.PAUSED
:
2364 task
.status
= task
.CANCELLED
2365 # Call run, so the partial file gets deleted
2368 self
.update_episode_list_icons([task
.url
for task
in tasks
])
2369 self
.play_or_download()
2371 # Update the tab title and downloads list
2372 self
.update_downloads_list()
2374 def new_episodes_show(self
, episodes
, notification
=False):
2375 if gpodder
.ui
.maemo
:
2377 ('maemo_markup', None, None, _('Episode')),
2379 show_notification
= notification
2382 ('title_markup', None, None, _('Episode')),
2383 ('channel_prop', None, None, _('Podcast')),
2384 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2385 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2387 show_notification
= False
2389 instructions
= _('Select the episodes you want to download:')
2391 if self
.new_episodes_window
is not None:
2392 self
.new_episodes_window
.main_window
.destroy()
2393 self
.new_episodes_window
= None
2395 def download_episodes_callback(episodes
):
2396 self
.new_episodes_window
= None
2397 self
.download_episode_list(episodes
)
2399 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
2400 title
=_('New episodes available'), \
2401 instructions
=instructions
, \
2402 episodes
=episodes
, \
2404 selected_default
=True, \
2405 stock_ok_button
= 'gpodder-download', \
2406 callback
=download_episodes_callback
, \
2407 remove_callback
=lambda e
: e
.mark_old(), \
2408 remove_action
=_('Mark as old'), \
2409 remove_finished
=self
.episode_new_status_changed
, \
2410 _config
=self
.config
, \
2411 show_notification
=show_notification
)
2413 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
2414 if not self
.offer_new_episodes():
2415 self
.show_message(_('Please check for new episodes later.'), \
2416 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
2418 def get_new_episodes(self
, channels
=None):
2419 if channels
is None:
2420 channels
= self
.channels
2422 for channel
in channels
:
2423 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
2424 episodes
.append(episode
)
2428 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
2429 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
2430 # The sync process might have updated the status of episodes,
2431 # therefore persist the database here to avoid losing data
2434 def on_cleanup_ipod_activate(self
, widget
, *args
):
2435 self
.sync_ui
.on_cleanup_device()
2437 def on_manage_device_playlist(self
, widget
):
2438 self
.sync_ui
.on_manage_device_playlist()
2440 def show_hide_tray_icon(self
):
2441 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
2442 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
2443 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
2444 self
.tray_icon
.set_visible(False)
2446 self
.tray_icon
= None
2448 if self
.config
.minimize_to_tray
and self
.tray_icon
:
2449 self
.tray_icon
.set_visible(self
.is_iconified())
2450 elif self
.tray_icon
:
2451 self
.tray_icon
.set_visible(True)
2453 def on_itemShowToolbar_activate(self
, widget
):
2454 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
2456 def on_itemShowDescription_activate(self
, widget
):
2457 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
2459 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
2460 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
2461 if self
.config
.podcast_list_hide_boring
:
2462 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2464 self
.podcast_list_model
.set_view_mode(-1)
2466 def on_item_view_podcasts_changed(self
, radioaction
, current
):
2468 if current
== self
.item_view_podcasts_all
:
2469 self
.podcast_list_model
.set_view_mode(-1)
2470 elif current
== self
.item_view_podcasts_downloaded
:
2471 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2472 elif current
== self
.item_view_podcasts_unplayed
:
2473 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2475 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
2477 def on_item_view_episodes_changed(self
, radioaction
, current
):
2478 if current
== self
.item_view_episodes_all
:
2479 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
2480 elif current
== self
.item_view_episodes_undeleted
:
2481 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
2482 elif current
== self
.item_view_episodes_downloaded
:
2483 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2484 elif current
== self
.item_view_episodes_unplayed
:
2485 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2487 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
2489 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
2490 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2492 def update_item_device( self
):
2493 if not gpodder
.ui
.fremantle
:
2494 if self
.config
.device_type
!= 'none':
2495 self
.itemDevice
.set_visible(True)
2496 self
.itemDevice
.label
= self
.get_device_name()
2498 self
.itemDevice
.set_visible(False)
2500 def properties_closed( self
):
2501 self
.show_hide_tray_icon()
2502 self
.update_item_device()
2503 if gpodder
.ui
.maemo
:
2504 selection
= self
.treeAvailable
.get_selection()
2505 if self
.config
.maemo_enable_gestures
or \
2506 self
.config
.enable_fingerscroll
:
2507 selection
.set_mode(gtk
.SELECTION_SINGLE
)
2509 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
2511 def on_itemPreferences_activate(self
, widget
, *args
):
2512 gPodderPreferences(self
.gPodder
, _config
=self
.config
, \
2513 callback_finished
=self
.properties_closed
, \
2514 user_apps_reader
=self
.user_apps_reader
)
2516 def on_itemDependencies_activate(self
, widget
):
2517 gPodderDependencyManager(self
.gPodder
)
2519 def require_my_gpodder_authentication(self
):
2520 if not self
.config
.my_gpodder_username
or not self
.config
.my_gpodder_password
:
2521 success
, authentication
= self
.show_login_dialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username
=self
.config
.my_gpodder_username
, password
=self
.config
.my_gpodder_password
, username_prompt
=_('E-Mail Address'), register_callback
=lambda: util
.open_website('http://my.gpodder.org/register'))
2522 if success
and authentication
[0] and authentication
[1]:
2523 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
= authentication
2530 def my_gpodder_offer_autoupload(self
):
2531 if not self
.config
.my_gpodder_autoupload
:
2532 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')):
2533 self
.config
.my_gpodder_autoupload
= True
2535 def on_download_from_mygpo(self
, widget
=None):
2536 if self
.require_my_gpodder_authentication():
2537 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2538 opml_data
= client
.download_subscriptions()
2539 if len(opml_data
) > 0:
2540 fp
= open(gpodder
.subscription_file
, 'w')
2543 (added
, skipped
) = (0, 0)
2544 i
= opml
.Importer(gpodder
.subscription_file
)
2546 existing
= [c
.url
for c
in self
.channels
]
2547 urls
= [item
['url'] for item
in i
.items
if item
['url'] not in existing
]
2549 skipped
= len(i
.items
) - len(urls
)
2552 self
.add_podcast_list(urls
)
2554 self
.my_gpodder_offer_autoupload()
2556 self
.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added
, skipped
), _('Result of subscription download'), widget
=self
.treeChannels
)
2557 elif widget
is not None:
2558 self
.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget
=self
.treeChannels
)
2560 self
.config
.my_gpodder_password
= ''
2561 self
.on_download_from_mygpo(widget
)
2563 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2565 def on_upload_to_mygpo(self
, widget
):
2566 if self
.require_my_gpodder_authentication():
2567 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2568 self
.save_channels_opml()
2569 success
, messages
= client
.upload_subscriptions(gpodder
.subscription_file
)
2570 if widget
is not None:
2572 self
.show_message('\n'.join(messages
), _('Results of upload'), important
=True)
2573 self
.config
.my_gpodder_password
= ''
2574 self
.on_upload_to_mygpo(widget
)
2576 self
.my_gpodder_offer_autoupload()
2577 self
.show_message('\n'.join(messages
), _('Results of upload'), widget
=self
.treeChannels
)
2579 log('Upload to my.gpodder.org failed, but widget is None!', sender
=self
)
2580 elif widget
is not None:
2581 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2583 def on_itemAddChannel_activate(self
, widget
=None):
2584 gPodderAddPodcast(self
.gPodder
, \
2585 add_urls_callback
=self
.add_podcast_list
)
2587 def on_itemEditChannel_activate(self
, widget
, *args
):
2588 if self
.active_channel
is None:
2589 title
= _('No podcast selected')
2590 message
= _('Please select a podcast in the podcasts list to edit.')
2591 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2594 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
2595 gPodderChannel(self
.main_window
, \
2596 channel
=self
.active_channel
, \
2597 callback_closed
=callback_closed
, \
2598 cover_downloader
=self
.cover_downloader
)
2600 def on_itemRemoveChannel_activate(self
, widget
, *args
):
2601 if self
.active_channel
is None:
2602 title
= _('No podcast selected')
2603 message
= _('Please select a podcast in the podcasts list to remove.')
2604 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2608 if gpodder
.ui
.desktop
:
2609 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2610 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
2611 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
2613 title
= _('Remove podcast and episodes?')
2614 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
2616 dialog
.set_title(title
)
2617 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2619 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
2620 dialog
.vbox
.pack_start(cb_ask
)
2622 result
= (dialog
.run() == gtk
.RESPONSE_YES
)
2623 keep_episodes
= cb_ask
.get_active()
2625 elif gpodder
.ui
.diablo
:
2626 result
= self
.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2627 keep_episodes
= False
2628 elif gpodder
.ui
.fremantle
:
2630 keep_episodes
= False
2633 # delete downloaded episodes only if checkbox is unchecked
2635 log('Not removing downloaded episodes', sender
=self
)
2637 self
.active_channel
.remove_downloaded()
2639 # Clean up downloads and download directories
2640 self
.clean_up_downloads()
2642 # cancel any active downloads from this channel
2643 for episode
in self
.active_channel
.get_all_episodes():
2644 self
.download_status_model
.cancel_by_url(episode
.url
)
2646 # get the URL of the podcast we want to select next
2647 position
= self
.channels
.index(self
.active_channel
)
2648 if position
== len(self
.channels
)-1:
2649 # this is the last podcast, so select the URL
2650 # of the item before this one (i.e. the "new last")
2651 select_url
= self
.channels
[position
-1].url
2653 # there is a podcast after the deleted one, so
2654 # we simply select the one that comes after it
2655 select_url
= self
.channels
[position
+1].url
2657 title
= self
.active_channel
.title
2659 # Remove the channel
2660 self
.active_channel
.delete(purge
=not keep_episodes
)
2661 self
.channels
.remove(self
.active_channel
)
2662 self
.channel_list_changed
= True
2663 self
.save_channels_opml()
2665 if gpodder
.ui
.fremantle
:
2666 self
.show_message(_('Podcast removed: %s') % title
)
2668 # Re-load the channels and select the desired new channel
2669 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
2671 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
2672 self
.update_podcasts_tab()
2674 def get_opml_filter(self
):
2675 filter = gtk
.FileFilter()
2676 filter.add_pattern('*.opml')
2677 filter.add_pattern('*.xml')
2678 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2681 def on_item_import_from_file_activate(self
, widget
, filename
=None):
2682 if filename
is None:
2683 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2684 # FIXME: Hildonization on Fremantle
2685 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2686 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2687 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2688 elif gpodder
.ui
.diablo
:
2689 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2690 dlg
.set_filter(self
.get_opml_filter())
2691 response
= dlg
.run()
2693 if response
== gtk
.RESPONSE_OK
:
2694 filename
= dlg
.get_filename()
2697 if filename
is not None:
2698 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
2699 custom_title
=_('Import podcasts from OPML file'), \
2700 add_urls_callback
=self
.add_podcast_list
, \
2701 hide_url_entry
=True)
2702 dir.download_opml_file(filename
)
2704 def on_itemExportChannels_activate(self
, widget
, *args
):
2705 if not self
.channels
:
2706 title
= _('Nothing to export')
2707 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2708 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2711 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2712 # FIXME: Hildonization on Fremantle
2713 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
2714 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2715 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
2716 elif gpodder
.ui
.diablo
:
2717 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
2718 dlg
.set_filter(self
.get_opml_filter())
2719 response
= dlg
.run()
2720 if response
== gtk
.RESPONSE_OK
:
2721 filename
= dlg
.get_filename()
2723 exporter
= opml
.Exporter( filename
)
2724 if exporter
.write(self
.channels
):
2725 if len(self
.channels
) == 1:
2726 title
= _('One subscription exported')
2728 title
= _('%d subscriptions exported') % len(self
.channels
)
2729 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
2731 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
2735 def on_itemImportChannels_activate(self
, widget
, *args
):
2736 if gpodder
.ui
.fremantle
:
2737 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
2738 self
.config
.toplist_url
, \
2739 self
.config
.opml_url
, \
2740 self
.add_podcast_list
, \
2741 self
.on_itemAddChannel_activate
, \
2742 self
.on_download_from_mygpo
, \
2743 self
.show_text_edit_dialog
)
2745 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
2746 add_urls_callback
=self
.add_podcast_list
)
2747 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
2749 def on_homepage_activate(self
, widget
, *args
):
2750 util
.open_website(gpodder
.__url
__)
2752 def on_wiki_activate(self
, widget
, *args
):
2753 util
.open_website('http://wiki.gpodder.org/')
2755 def on_bug_tracker_activate(self
, widget
, *args
):
2756 if gpodder
.ui
.maemo
:
2757 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2759 util
.open_website('http://bugs.gpodder.org/')
2761 def on_shop_activate(self
, widget
, *args
):
2762 util
.open_website('http://gpodder.org/shop')
2764 def on_wishlist_activate(self
, widget
, *args
):
2765 util
.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2767 def on_itemAbout_activate(self
, widget
, *args
):
2768 dlg
= gtk
.AboutDialog()
2769 dlg
.set_name('gPodder')
2770 dlg
.set_version(gpodder
.__version
__)
2771 dlg
.set_copyright(gpodder
.__copyright
__)
2772 dlg
.set_website(gpodder
.__url
__)
2773 dlg
.set_translator_credits( _('translator-credits'))
2774 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
2776 if gpodder
.ui
.desktop
:
2777 # For the "GUI" version, we add some more
2778 # items to the about dialog (credits and logo)
2781 'Thomas Perl <thpinfo.com>',
2784 if os
.path
.exists(gpodder
.credits_file
):
2785 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
2786 app_authors
+= ['', _('Patches, bug reports and donations by:')]
2787 app_authors
+= credits
2789 dlg
.set_authors(app_authors
)
2791 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
2793 dlg
.set_logo_icon_name('gpodder')
2794 elif gpodder
.ui
.fremantle
:
2795 for parent
in dlg
.vbox
.get_children():
2796 for child
in parent
.get_children():
2797 if isinstance(child
, gtk
.Label
):
2798 child
.set_selectable(False)
2802 def on_wNotebook_switch_page(self
, widget
, *args
):
2804 if gpodder
.ui
.maemo
:
2805 self
.tool_downloads
.set_active(page_num
== 1)
2806 page
= self
.wNotebook
.get_nth_page(page_num
)
2807 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2808 if page_num
== 0 and self
.active_channel
is not None:
2809 self
.set_title(self
.active_channel
.title
)
2811 self
.set_title(tab_label
)
2813 self
.play_or_download()
2814 self
.menuChannels
.set_sensitive(True)
2815 self
.menuSubscriptions
.set_sensitive(True)
2816 # The message area in the downloads tab should be hidden
2817 # when the user switches away from the downloads tab
2818 if self
.message_area
is not None:
2819 self
.message_area
.hide()
2820 self
.message_area
= None
2822 self
.menuChannels
.set_sensitive(False)
2823 self
.menuSubscriptions
.set_sensitive(False)
2824 if gpodder
.ui
.desktop
:
2825 self
.toolDownload
.set_sensitive(False)
2826 self
.toolPlay
.set_sensitive(False)
2827 self
.toolTransfer
.set_sensitive(False)
2828 self
.toolCancel
.set_sensitive(False)
2830 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
2831 # double-click action of the podcast list or enter
2832 self
.treeChannels
.set_cursor(path
)
2834 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2835 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2837 if model
is not None and iter is not None:
2838 old_active_channel
= self
.active_channel
2839 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
2841 if self
.active_channel
== old_active_channel
:
2844 if gpodder
.ui
.maemo
:
2845 self
.set_title(self
.active_channel
.title
)
2846 self
.itemEditChannel
.set_visible(True)
2847 self
.itemRemoveChannel
.set_visible(True)
2849 self
.active_channel
= None
2850 self
.itemEditChannel
.set_visible(False)
2851 self
.itemRemoveChannel
.set_visible(False)
2853 self
.update_episode_list_model()
2855 def on_btnEditChannel_clicked(self
, widget
, *args
):
2856 self
.on_itemEditChannel_activate( widget
, args
)
2858 def get_selected_episodes(self
):
2859 """Get a list of selected episodes from treeAvailable"""
2860 selection
= self
.treeAvailable
.get_selection()
2861 model
, paths
= selection
.get_selected_rows()
2863 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
2866 def on_transfer_selected_episodes(self
, widget
):
2867 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
2869 def on_playback_selected_episodes(self
, widget
):
2870 self
.playback_episodes(self
.get_selected_episodes())
2872 def on_shownotes_selected_episodes(self
, widget
):
2873 episodes
= self
.get_selected_episodes()
2875 episode
= episodes
.pop(0)
2876 self
.show_episode_shownotes(episode
)
2878 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
2880 def on_download_selected_episodes(self
, widget
):
2881 episodes
= self
.get_selected_episodes()
2882 self
.download_episode_list(episodes
)
2883 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
2884 self
.play_or_download()
2886 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
2887 """Double-click/enter action handler for treeAvailable"""
2888 # We should only have one one selected as it was double clicked!
2889 e
= self
.get_selected_episodes()[0]
2891 if (self
.config
.double_click_episode_action
== 'download'):
2892 # If the episode has already been downloaded and exists then play it
2893 if e
.was_downloaded(and_exists
=True):
2894 self
.playback_episodes(self
.get_selected_episodes())
2895 # else download it if it is not already downloading
2896 elif not self
.episode_is_downloading(e
):
2897 self
.download_episode_list([e
])
2898 self
.update_episode_list_icons([e
.url
])
2899 self
.play_or_download()
2900 elif (self
.config
.double_click_episode_action
== 'stream'):
2901 # If we happen to have downloaded this episode simple play it
2902 if e
.was_downloaded(and_exists
=True):
2903 self
.playback_episodes(self
.get_selected_episodes())
2904 # else if streaming is possible stream it
2905 elif self
.streaming_possible():
2906 self
.playback_episodes(self
.get_selected_episodes())
2908 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
2909 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
2911 # default action is to display show notes
2912 self
.on_shownotes_selected_episodes(widget
)
2914 def show_episode_shownotes(self
, episode
):
2915 if self
.episode_shownotes_window
is None:
2916 log('First-time use of episode window --- creating', sender
=self
)
2917 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
2918 _download_episode_list
=self
.download_episode_list
, \
2919 _playback_episodes
=self
.playback_episodes
, \
2920 _delete_episode_list
=self
.delete_episode_list
, \
2921 _episode_list_status_changed
=self
.episode_list_status_changed
, \
2922 _cancel_task_list
=self
.cancel_task_list
, \
2923 _episode_is_downloading
=self
.episode_is_downloading
)
2924 self
.episode_shownotes_window
.show(episode
)
2925 if self
.episode_is_downloading(episode
):
2926 self
.update_downloads_list()
2928 def auto_update_procedure(self
, first_run
=False):
2929 log('auto_update_procedure() got called', sender
=self
)
2930 if not first_run
and self
.config
.auto_update_feeds
and self
.is_iconified():
2931 self
.update_feed_cache(force_update
=True)
2933 next_update
= 60*1000*self
.config
.auto_update_frequency
2934 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2937 def on_treeDownloads_row_activated(self
, widget
, *args
):
2938 # Use the standard way of working on the treeview
2939 selection
= self
.treeDownloads
.get_selection()
2940 (model
, paths
) = selection
.get_selected_rows()
2941 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
2943 for tree_row_reference
, task
in selected_tasks
:
2944 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
2945 task
.status
= task
.PAUSED
2946 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
2947 self
.download_queue_manager
.add_task(task
)
2948 self
.enable_download_list_update()
2949 elif task
.status
== task
.DONE
:
2950 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
2952 self
.play_or_download()
2954 # Update the tab title and downloads list
2955 self
.update_downloads_list()
2957 def on_item_cancel_download_activate(self
, widget
):
2958 if self
.wNotebook
.get_current_page() == 0:
2959 selection
= self
.treeAvailable
.get_selection()
2960 (model
, paths
) = selection
.get_selected_rows()
2961 urls
= [model
.get_value(model
.get_iter(path
), \
2962 self
.episode_list_model
.C_URL
) for path
in paths
]
2963 selected_tasks
= [task
for task
in self
.download_tasks_seen \
2964 if task
.url
in urls
]
2966 selection
= self
.treeDownloads
.get_selection()
2967 (model
, paths
) = selection
.get_selected_rows()
2968 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
2969 self
.download_status_model
.C_TASK
) for path
in paths
]
2970 self
.cancel_task_list(selected_tasks
)
2972 def on_btnCancelAll_clicked(self
, widget
, *args
):
2973 self
.cancel_task_list(self
.download_tasks_seen
)
2975 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2976 if self
.wNotebook
.get_current_page() == 1:
2977 # Downloads tab visibile - skip (for now)
2980 episodes
= self
.get_selected_episodes()
2981 self
.delete_episode_list(episodes
)
2983 def on_key_press(self
, widget
, event
):
2984 # Allow tab switching with Ctrl + PgUp/PgDown
2985 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
2986 if event
.keyval
== gtk
.keysyms
.Page_Up
:
2987 self
.wNotebook
.prev_page()
2989 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
2990 self
.wNotebook
.next_page()
2993 # After this code we only handle Maemo hardware keys,
2994 # so if we are not a Maemo app, we don't do anything
2995 if not gpodder
.ui
.maemo
:
2999 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3001 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3004 if diff
!= 0 and not self
.currently_updating
:
3005 selection
= self
.treeChannels
.get_selection()
3006 (model
, iter) = selection
.get_selected()
3007 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3008 selection
.select_path(new_path
)
3009 self
.treeChannels
.set_cursor(new_path
)
3014 def on_iconify(self
):
3016 self
.gPodder
.set_skip_taskbar_hint(True)
3017 if self
.config
.minimize_to_tray
:
3018 self
.tray_icon
.set_visible(True)
3020 self
.gPodder
.set_skip_taskbar_hint(False)
3022 def on_uniconify(self
):
3024 self
.gPodder
.set_skip_taskbar_hint(False)
3025 if self
.config
.minimize_to_tray
:
3026 self
.tray_icon
.set_visible(False)
3028 self
.gPodder
.set_skip_taskbar_hint(False)
3030 def uniconify_main_window(self
):
3031 if self
.is_iconified():
3032 self
.gPodder
.present()
3034 def iconify_main_window(self
):
3035 if not self
.is_iconified():
3036 self
.gPodder
.iconify()
3038 def update_podcasts_tab(self
):
3039 if len(self
.channels
):
3040 if gpodder
.ui
.fremantle
:
3041 self
.button_podcasts
.set_value(_('%d subscriptions') % len(self
.channels
))
3043 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3045 if gpodder
.ui
.fremantle
:
3046 self
.button_podcasts
.set_value(_('No subscriptions'))
3048 self
.label2
.set_text(_('Podcasts'))
3050 @dbus.service
.method(gpodder
.dbus_interface
)
3051 def show_gui_window(self
):
3052 self
.gPodder
.present()
3054 @dbus.service
.method(gpodder
.dbus_interface
)
3055 def subscribe_to_url(self
, url
):
3056 gPodderAddPodcast(self
.gPodder
,
3057 add_urls_callback
=self
.add_podcast_list
,
3060 @dbus.service
.method(gpodder
.dbus_interface
)
3061 def mark_episode_played(self
, filename
):
3062 if filename
is None:
3065 for channel
in self
.channels
:
3066 for episode
in channel
.get_all_episodes():
3067 fn
= episode
.local_filename(create
=False, check_only
=True)
3069 episode
.mark(is_played
=True)
3071 self
.update_episode_list_icons([episode
.url
])
3072 self
.update_podcast_list_model([episode
.channel
.url
])
3078 def main(options
=None):
3079 gobject
.threads_init()
3080 gobject
.set_application_name('gPodder')
3082 if gpodder
.ui
.diablo
:
3083 # Try to enable the custom icon theme for gPodder on Maemo
3084 settings
= gtk
.settings_get_default()
3085 settings
.set_string_property('gtk-icon-theme-name', \
3086 'gpodder', __file__
)
3088 gtk
.window_set_default_icon_name('gpodder')
3089 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3092 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3093 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3094 except dbus
.exceptions
.DBusException
, dbe
:
3095 log('Warning: Cannot get "on the bus".', traceback
=True)
3096 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3097 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3098 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3099 dlg
.set_title('gPodder')
3104 util
.make_directory(gpodder
.home
)
3105 config
= UIConfig(gpodder
.config_file
)
3107 if gpodder
.ui
.diablo
:
3108 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3109 # folder exists there (allow moving "gpodder" between SD cards or USB)
3110 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3111 if not os
.path
.exists(config
.download_dir
):
3112 log('Downloads might have been moved. Trying to locate them...')
3113 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3114 dir = os
.path
.join(basedir
, 'gpodder')
3115 if os
.path
.exists(dir):
3116 log('Downloads found in: %s', dir)
3117 config
.download_dir
= dir
3120 log('Downloads NOT FOUND in %s', dir)
3122 if config
.enable_fingerscroll
:
3123 BuilderWidget
.use_fingerscroll
= True
3124 elif gpodder
.ui
.fremantle
:
3125 # FIXME: Move download_dir from ~/gPodder-Podcasts to default setting
3128 gp
= gPodder(bus_name
, config
)
3131 if options
.subscribe
:
3132 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)