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
)
170 # Initialize portrait mode / rotation manager
171 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
172 self
.main_window
, gpodder
.__version
__)
174 self
.bluetooth_available
= False
176 self
.bluetooth_available
= util
.bluetooth_available()
177 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
179 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
180 if not gpodder
.ui
.fremantle
:
181 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
182 self
.main_window
.show()
184 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
186 self
.config
.add_observer(self
.on_config_changed
)
188 self
.tray_icon
= None
189 self
.episode_shownotes_window
= None
190 self
.new_episodes_window
= None
192 if gpodder
.ui
.desktop
:
193 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
194 self
.main_window
, self
.show_confirmation
, \
195 self
.update_episode_list_icons
, \
196 self
.update_podcast_list_model
, self
.toolPreferences
, \
197 gPodderEpisodeSelector
)
201 self
.download_status_model
= DownloadStatusModel()
202 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
204 if gpodder
.ui
.desktop
:
205 self
.show_hide_tray_icon()
206 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
207 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
209 if not gpodder
.ui
.fremantle
:
210 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
211 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
212 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
213 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
215 # When the amount of maximum downloads changes, notify the queue manager
216 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_and_retire_threads()
217 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
219 self
.default_title
= 'gPodder'
220 if gpodder
.__version
__.rfind('git') != -1:
221 self
.set_title('gPodder %s' % gpodder
.__version
__)
223 title
= self
.gPodder
.get_title()
224 if title
is not None:
225 self
.set_title(title
)
227 self
.set_title(_('gPodder'))
229 self
.cover_downloader
= CoverDownloader()
231 # Generate list models for podcasts and their episodes
232 self
.podcast_list_model
= PodcastListModel(self
.config
.podcast_list_icon_size
, self
.cover_downloader
)
234 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
235 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
237 if gpodder
.ui
.fremantle
:
238 self
.button_subscribe
.set_name('HildonButton-thumb')
239 self
.button_podcasts
.set_name('HildonButton-thumb')
240 self
.button_downloads
.set_name('HildonButton-thumb')
242 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
243 while gtk
.events_pending():
244 gtk
.main_iteration(False)
246 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
247 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
248 show_episode_shownotes
=self
.show_episode_shownotes
, \
249 update_podcast_list_model
=self
.update_podcast_list_model
, \
250 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
251 item_view_episodes_all
=self
.item_view_episodes_all
, \
252 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
253 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
254 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
)
256 def on_podcast_selected(channel
):
257 self
.active_channel
= channel
258 self
.update_episode_list_model()
259 self
.episodes_window
.channel
= self
.active_channel
260 self
.episodes_window
.show()
262 self
.podcasts_window
= gPodderPodcasts(self
.main_window
, \
263 show_podcast_episodes
=on_podcast_selected
, \
264 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
265 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
266 item_view_podcasts_all
=self
.item_view_podcasts_all
, \
267 item_view_podcasts_downloaded
=self
.item_view_podcasts_downloaded
, \
268 item_view_podcasts_unplayed
=self
.item_view_podcasts_unplayed
)
270 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
271 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
272 on_btnCleanUpDownloads_clicked
=self
.on_btnCleanUpDownloads_clicked
, \
273 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
274 downloads_list_get_selection
=self
.downloads_list_get_selection
)
275 self
.treeChannels
= self
.podcasts_window
.treeview
276 self
.treeAvailable
= self
.episodes_window
.treeview
277 self
.treeDownloads
= self
.downloads_window
.treeview
279 # Init the treeviews that we use
280 self
.init_podcast_list_treeview()
281 self
.init_episode_list_treeview()
282 self
.init_download_list_treeview()
284 if self
.config
.podcast_list_hide_boring
:
285 self
.item_view_hide_boring_podcasts
.set_active(True)
287 self
.currently_updating
= False
290 self
.context_menu_mouse_button
= 1
292 self
.context_menu_mouse_button
= 3
294 if self
.config
.start_iconified
:
295 self
.iconify_main_window()
297 self
.download_tasks_seen
= set()
298 self
.download_list_update_enabled
= False
299 self
.last_download_count
= 0
301 # Subscribed channels
302 self
.active_channel
= None
303 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
304 self
.channel_list_changed
= True
305 self
.update_podcasts_tab()
307 # load list of user applications for audio playback
308 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
310 time
.sleep(3) # give other parts of gpodder a chance to start up
311 self
.user_apps_reader
.read()
312 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
313 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
314 threading
.Thread(target
=read_apps
).start()
316 # Set the "Device" menu item for the first time
317 if gpodder
.ui
.desktop
:
318 self
.update_item_device()
320 # Now, update the feed cache, when everything's in place
321 if not gpodder
.ui
.fremantle
:
322 self
.btnUpdateFeeds
.show()
323 self
.updating_feed_cache
= False
324 self
.feed_cache_update_cancelled
= False
325 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
327 # Look for partial file downloads
328 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
331 self
.message_area
= None
333 resumable_episodes
= []
334 if len(partial_files
) > 0:
335 for f
in partial_files
:
336 correct_name
= f
[:-len('.partial')] # strip ".partial"
337 log('Searching episode for file: %s', correct_name
, sender
=self
)
338 found_episode
= False
339 for c
in self
.channels
:
340 for e
in c
.get_all_episodes():
341 if e
.local_filename(create
=False, check_only
=True) == correct_name
:
342 log('Found episode: %s', e
.title
, sender
=self
)
343 resumable_episodes
.append(e
)
349 if not found_episode
:
350 log('Partial file without episode: %s', f
, sender
=self
)
353 if len(resumable_episodes
):
354 self
.download_episode_list_paused(resumable_episodes
)
355 if not gpodder
.ui
.fremantle
:
356 self
.message_area
= SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
357 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
358 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
359 self
.message_area
.show_all()
360 self
.wNotebook
.set_current_page(1)
362 self
.clean_up_downloads(delete_partial
=False)
364 self
.clean_up_downloads(delete_partial
=True)
366 # Start the auto-update procedure
367 self
.auto_update_procedure(first_run
=True)
369 # Delete old episodes if the user wishes to
370 if self
.config
.auto_remove_old_episodes
:
371 old_episodes
= self
.get_old_episodes()
372 if len(old_episodes
) > 0:
373 self
.delete_episode_list(old_episodes
, confirm
=False)
374 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
376 if gpodder
.ui
.fremantle
:
377 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
378 self
.button_subscribe
.set_sensitive(True)
379 self
.button_podcasts
.set_sensitive(True)
380 self
.button_downloads
.set_sensitive(True)
382 # First-time users should be asked if they want to see the OPML
383 if not self
.channels
and not gpodder
.ui
.fremantle
:
384 util
.idle_add(self
.on_itemUpdate_activate
)
386 def on_button_subscribe_clicked(self
, button
):
387 self
.on_itemImportChannels_activate(button
)
389 def on_button_podcasts_clicked(self
, widget
):
391 self
.podcasts_window
.show()
393 gPodderWelcome(self
.gPodder
, \
394 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
, \
395 setup_my_gpodder_callback
=self
.on_download_from_mygpo
)
397 def on_button_downloads_clicked(self
, widget
):
398 self
.downloads_window
.show()
400 def on_window_orientation_changed(self
, orientation
):
401 old_container
= self
.main_window
.get_child()
402 if orientation
== Orientation
.PORTRAIT
:
403 container
= gtk
.VButtonBox()
405 container
= gtk
.HButtonBox()
406 container
.set_layout(old_container
.get_layout())
407 for child
in old_container
.get_children():
408 if orientation
== Orientation
.LANDSCAPE
:
409 child
.set_alignment(0.5, 0.5, 0., 0.)
410 child
.set_property('width-request', 200)
412 child
.set_alignment(0.5, 0.5, .9, 0.)
413 child
.set_property('width-request', 350)
414 child
.reparent(container
)
416 self
.buttonbox
= container
417 self
.main_window
.remove(old_container
)
418 self
.main_window
.add(container
)
420 def on_treeview_podcasts_selection_changed(self
, selection
):
421 model
, iter = selection
.get_selected()
423 self
.active_channel
= None
424 self
.episode_list_model
.clear()
426 def on_treeview_button_pressed(self
, treeview
, event
):
427 if event
.window
!= treeview
.get_bin_window():
430 TreeViewHelper
.save_button_press_event(treeview
, event
)
432 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
433 TreeViewHelper
.ROLE_PODCASTS
:
434 return self
.currently_updating
436 return event
.button
== self
.context_menu_mouse_button
and \
439 def on_treeview_podcasts_button_released(self
, treeview
, event
):
440 if event
.window
!= treeview
.get_bin_window():
444 return self
.treeview_channels_handle_gestures(treeview
, event
)
446 return self
.treeview_channels_show_context_menu(treeview
, event
)
448 def on_treeview_episodes_button_released(self
, treeview
, event
):
449 if event
.window
!= treeview
.get_bin_window():
453 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
454 return self
.treeview_available_handle_gestures(treeview
, event
)
456 return self
.treeview_available_show_context_menu(treeview
, event
)
458 def on_treeview_downloads_button_released(self
, treeview
, event
):
459 if event
.window
!= treeview
.get_bin_window():
462 return self
.treeview_downloads_show_context_menu(treeview
, event
)
464 def init_podcast_list_treeview(self
):
465 # Set up podcast channel tree view widget
466 self
.treeChannels
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(PodcastListModel
))
468 if gpodder
.ui
.fremantle
:
469 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
470 self
.item_view_podcasts_downloaded
.set_active(True)
471 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
472 self
.item_view_podcasts_unplayed
.set_active(True)
474 self
.item_view_podcasts_all
.set_active(True)
475 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
477 iconcolumn
= gtk
.TreeViewColumn('')
478 iconcell
= gtk
.CellRendererPixbuf()
479 iconcolumn
.pack_start(iconcell
, False)
480 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
481 self
.treeChannels
.append_column(iconcolumn
)
483 namecolumn
= gtk
.TreeViewColumn('')
484 namecell
= gtk
.CellRendererText()
485 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
486 namecolumn
.pack_start(namecell
, True)
487 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
489 iconcell
= gtk
.CellRendererPixbuf()
490 iconcell
.set_property('xalign', 1.0)
491 namecolumn
.pack_start(iconcell
, False)
492 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
493 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
494 self
.treeChannels
.append_column(namecolumn
)
496 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
498 # When no podcast is selected, clear the episode list model
499 selection
= self
.treeChannels
.get_selection()
500 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
502 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
504 def init_episode_list_treeview(self
):
505 self
.episode_list_model
= EpisodeListModel()
507 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
508 self
.item_view_episodes_undeleted
.set_active(True)
509 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
510 self
.item_view_episodes_downloaded
.set_active(True)
511 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
512 self
.item_view_episodes_unplayed
.set_active(True)
514 self
.item_view_episodes_all
.set_active(True)
516 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
518 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
520 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
522 iconcell
= gtk
.CellRendererPixbuf()
524 iconcell
.set_fixed_size(50, 50)
525 status_column_label
= ''
527 status_column_label
= _('Status')
528 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
530 namecell
= gtk
.CellRendererText()
531 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
532 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
533 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
534 namecolumn
.set_resizable(True)
535 namecolumn
.set_expand(True)
537 sizecell
= gtk
.CellRendererText()
538 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
540 releasecell
= gtk
.CellRendererText()
541 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
543 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
544 itemcolumn
.set_reorderable(True)
545 self
.treeAvailable
.append_column(itemcolumn
)
548 sizecolumn
.set_visible(False)
549 releasecolumn
.set_visible(False)
551 self
.treeAvailable
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(EpisodeListModel
))
553 selection
= self
.treeAvailable
.get_selection()
554 if gpodder
.ui
.diablo
:
555 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
556 selection
.set_mode(gtk
.SELECTION_SINGLE
)
558 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
559 elif gpodder
.ui
.fremantle
:
560 selection
.set_mode(gtk
.SELECTION_SINGLE
)
562 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
563 # Update the sensitivity of the toolbar buttons on the Desktop
564 selection
.connect('changed', lambda s
: self
.play_or_download())
566 if gpodder
.ui
.diablo
:
567 # Set up the tap-and-hold context menu for podcasts
569 menu
.append(self
.itemUpdateChannel
.create_menu_item())
570 menu
.append(self
.itemEditChannel
.create_menu_item())
571 menu
.append(gtk
.SeparatorMenuItem())
572 menu
.append(self
.itemRemoveChannel
.create_menu_item())
573 menu
.append(gtk
.SeparatorMenuItem())
574 item
= gtk
.ImageMenuItem(_('Close this menu'))
575 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
579 menu
= self
.set_finger_friendly(menu
)
580 self
.treeChannels
.tap_and_hold_setup(menu
)
583 def init_download_list_treeview(self
):
584 # enable multiple selection support
585 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
586 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
588 # columns and renderers for "download progress" tab
589 # First column: [ICON] Episodename
590 column
= gtk
.TreeViewColumn(_('Episode'))
592 cell
= gtk
.CellRendererPixbuf()
594 cell
.set_fixed_size(50, 50)
595 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
596 column
.pack_start(cell
, expand
=False)
597 column
.add_attribute(cell
, 'stock-id', \
598 DownloadStatusModel
.C_ICON_NAME
)
600 cell
= gtk
.CellRendererText()
601 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
602 column
.pack_start(cell
, expand
=True)
603 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
604 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
605 column
.set_expand(True)
606 self
.treeDownloads
.append_column(column
)
608 # Second column: Progress
609 cell
= gtk
.CellRendererProgress()
610 cell
.set_property('yalign', .5)
611 cell
.set_property('ypad', 6)
612 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
613 value
=DownloadStatusModel
.C_PROGRESS
, \
614 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
615 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
616 column
.set_expand(False)
617 column
.set_property('min-width', 150)
618 column
.set_property('max-width', 150)
619 self
.treeDownloads
.append_column(column
)
621 self
.treeDownloads
.set_model(self
.download_status_model
)
622 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
624 def on_treeview_expose_event(self
, treeview
, event
):
625 if event
.window
== treeview
.get_bin_window():
626 model
= treeview
.get_model()
627 if (model
is not None and model
.get_iter_first() is not None):
630 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
631 ctx
= event
.window
.cairo_create()
632 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
633 event
.area
.width
, event
.area
.height
)
636 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
638 if role
== TreeViewHelper
.ROLE_EPISODES
:
639 if self
.currently_updating
:
640 text
= _('Loading episodes') + '...'
641 elif self
.config
.episode_list_view_mode
!= \
642 EpisodeListModel
.VIEW_ALL
:
643 text
= _('No episodes in current view')
645 text
= _('No episodes available')
646 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
647 if self
.config
.episode_list_view_mode
!= \
648 EpisodeListModel
.VIEW_ALL
and \
649 self
.config
.podcast_list_hide_boring
and \
650 len(self
.channels
) > 0:
651 text
= _('No podcasts in this view')
653 text
= _('No subscriptions')
654 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
655 text
= _('No active downloads')
657 raise Exception('on_treeview_expose_event: unknown role')
659 if gpodder
.ui
.fremantle
:
660 from gpodder
.gtkui
.frmntl
import style
661 font_desc
= style
.get_font_desc('LargeSystemFont')
665 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
)
669 def enable_download_list_update(self
):
670 if not self
.download_list_update_enabled
:
671 gobject
.timeout_add(1500, self
.update_downloads_list
)
672 self
.download_list_update_enabled
= True
674 def on_btnCleanUpDownloads_clicked(self
, button
):
675 model
= self
.download_status_model
677 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
678 changed_episode_urls
= []
679 for row_reference
, task
in all_tasks
:
680 if task
.status
in (task
.DONE
, task
.CANCELLED
, task
.FAILED
):
681 model
.remove(model
.get_iter(row_reference
.get_path()))
683 # We don't "see" this task anymore - remove it;
684 # this is needed, so update_episode_list_icons()
685 # below gets the correct list of "seen" tasks
686 self
.download_tasks_seen
.remove(task
)
687 except KeyError, key_error
:
688 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
689 changed_episode_urls
.append(task
.url
)
690 # Tell the task that it has been removed (so it can clean up)
691 task
.removed_from_list()
693 # Tell the podcasts tab to update icons for our removed podcasts
694 self
.update_episode_list_icons(changed_episode_urls
)
696 # Tell the shownotes window that we have removed the episode
697 if self
.episode_shownotes_window
is not None and \
698 self
.episode_shownotes_window
.episode
is not None and \
699 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
700 self
.episode_shownotes_window
._download
_status
_changed
(None)
702 # Update the tab title and downloads list
703 self
.update_downloads_list()
705 def on_tool_downloads_toggled(self
, toolbutton
):
706 if toolbutton
.get_active():
707 self
.wNotebook
.set_current_page(1)
709 self
.wNotebook
.set_current_page(0)
711 def update_downloads_list(self
):
713 model
= self
.download_status_model
715 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
716 total_speed
, total_size
, done_size
= 0, 0, 0
718 # Keep a list of all download tasks that we've seen
719 download_tasks_seen
= set()
721 # Remember the DownloadTask object for the episode that
722 # has been opened in the episode shownotes dialog (if any)
723 if self
.episode_shownotes_window
is not None:
724 shownotes_episode
= self
.episode_shownotes_window
.episode
725 shownotes_task
= None
727 shownotes_episode
= None
728 shownotes_task
= None
730 # Do not go through the list of the model is not (yet) available
735 self
.download_status_model
.request_update(row
.iter)
737 task
= row
[self
.download_status_model
.C_TASK
]
738 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
741 done_size
+= size
*progress
743 if shownotes_episode
is not None and \
744 shownotes_episode
.url
== task
.episode
.url
:
745 shownotes_task
= task
747 download_tasks_seen
.add(task
)
749 if status
== download
.DownloadTask
.DOWNLOADING
:
752 elif status
== download
.DownloadTask
.FAILED
:
754 elif status
== download
.DownloadTask
.DONE
:
756 elif status
== download
.DownloadTask
.QUEUED
:
758 elif status
== download
.DownloadTask
.PAUSED
:
763 # Remember which tasks we have seen after this run
764 self
.download_tasks_seen
= download_tasks_seen
766 if gpodder
.ui
.desktop
:
767 text
= [_('Downloads')]
768 if downloading
+ failed
+ finished
+ queued
> 0:
771 s
.append(_('%d active') % downloading
)
773 s
.append(_('%d failed') % failed
)
775 s
.append(_('%d done') % finished
)
777 s
.append(_('%d queued') % queued
)
778 text
.append(' (' + ', '.join(s
)+')')
779 self
.labelDownloads
.set_text(''.join(text
))
780 elif gpodder
.ui
.diablo
:
781 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
783 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
785 self
.tool_downloads
.set_label(_('Downloads'))
786 elif gpodder
.ui
.fremantle
:
787 if downloading
+ queued
> 0:
788 self
.button_downloads
.set_value(_('%d active') % (downloading
+queued
))
790 self
.button_downloads
.set_value(_('%d failed') % failed
)
792 self
.button_downloads
.set_value(_('%d paused') % paused
)
794 self
.button_downloads
.set_value(_('None active'))
796 title
= [self
.default_title
]
798 # We have to update all episodes/channels for which the status has
799 # changed. Accessing task.status_changed has the side effect of
800 # re-setting the changed flag, so we need to get the "changed" list
801 # of tuples first and split it into two lists afterwards
802 changed
= [(task
.url
, task
.podcast_url
) for task
in \
803 self
.download_tasks_seen
if task
.status_changed
]
804 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
805 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
807 count
= downloading
+ queued
810 title
.append( _('downloading one file'))
812 title
.append( _('downloading %d files') % count
)
815 percentage
= 100.0*done_size
/total_size
818 total_speed
= util
.format_filesize(total_speed
)
819 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
820 if self
.tray_icon
is not None:
821 # Update the tray icon status and progress bar
822 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
823 self
.tray_icon
.draw_progress_bar(percentage
/100.)
824 elif self
.last_download_count
> 0:
825 if self
.tray_icon
is not None:
826 # Update the tray icon status
827 self
.tray_icon
.set_status()
828 self
.tray_icon
.downloads_finished(self
.download_tasks_seen
)
829 if gpodder
.ui
.diablo
:
830 hildon
.hildon_banner_show_information(self
.gPodder
, None, 'gPodder: %s' % _('All downloads finished'))
831 log('All downloads have finished.', sender
=self
)
832 if self
.config
.cmd_all_downloads_complete
:
833 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
834 self
.last_download_count
= count
836 if not gpodder
.ui
.fremantle
:
837 self
.gPodder
.set_title(' - '.join(title
))
839 self
.update_episode_list_icons(episode_urls
)
840 if self
.episode_shownotes_window
is not None:
841 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
842 shownotes_task
!= self
.episode_shownotes_window
.task
:
843 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
844 self
.episode_shownotes_window
._download
_status
_progress
()
845 self
.play_or_download()
847 self
.update_podcast_list_model(channel_urls
)
849 if not self
.download_queue_manager
.are_queued_or_active_tasks():
850 self
.download_list_update_enabled
= False
852 return self
.download_list_update_enabled
854 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
855 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
856 # We return False here, so the update loop won't be called again,
857 # that's why we require the restart of gPodder in the message.
860 def on_config_changed(self
, name
, old_value
, new_value
):
861 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
862 self
.toolbar
.set_property('visible', new_value
)
863 elif name
== 'episode_list_descriptions':
864 self
.update_episode_list_model()
866 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
867 # With get_bin_window, we get the window that contains the rows without
868 # the header. The Y coordinate of this window will be the height of the
869 # treeview header. This is the amount we have to subtract from the
870 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
871 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
874 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
876 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
877 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
881 model
= treeview
.get_model()
882 iter = model
.get_iter(path
)
883 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
885 if role
== TreeViewHelper
.ROLE_EPISODES
:
886 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
887 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
888 id = model
.get_value(iter, PodcastListModel
.C_URL
)
890 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
891 if last_tooltip
is not None and last_tooltip
!= id:
892 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
894 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
896 if role
== TreeViewHelper
.ROLE_EPISODES
:
897 description
= model
.get_value(iter, EpisodeListModel
.C_DESCRIPTION_STRIPPED
)
898 if len(description
) > 400:
899 description
= description
[:398]+'[...]'
901 tooltip
.set_text(description
)
902 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
903 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
904 channel
.request_save_dir_size()
905 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
906 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
908 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
909 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
910 table
= gtk
.Table(rows
=3, columns
=3)
911 table
.set_row_spacings(5)
912 table
.set_col_spacings(5)
913 table
.set_border_width(5)
915 heading
= gtk
.Label()
916 heading
.set_alignment(0, 1)
917 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
918 table
.attach(heading
, 0, 1, 0, 1)
919 size_info
= gtk
.Label()
920 size_info
.set_alignment(1, 1)
921 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
922 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
923 table
.attach(size_info
, 2, 3, 0, 1)
925 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
927 if len(channel
.description
) < 500:
928 description
= channel
.description
930 pos
= channel
.description
.find('\n\n')
931 if pos
== -1 or pos
> 500:
932 description
= channel
.description
[:498]+'[...]'
934 description
= channel
.description
[:pos
]
936 description
= gtk
.Label(description
)
938 description
.set_markup(error_str
)
939 description
.set_alignment(0, 0)
940 description
.set_line_wrap(True)
941 table
.attach(description
, 0, 3, 2, 3)
944 tooltip
.set_custom(table
)
948 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
951 def treeview_allow_tooltips(self
, treeview
, allow
):
952 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
954 def update_m3u_playlist_clicked(self
, widget
):
955 if self
.active_channel
is not None:
956 self
.active_channel
.update_m3u_playlist()
957 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
959 def treeview_handle_context_menu_click(self
, treeview
, event
):
960 x
, y
= int(event
.x
), int(event
.y
)
961 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
963 selection
= treeview
.get_selection()
964 model
, paths
= selection
.get_selected_rows()
966 if path
is None or (path
not in paths
and \
967 event
.button
== self
.context_menu_mouse_button
):
968 # We have right-clicked, but not into the selection,
969 # assume we don't want to operate on the selection
972 if path
is not None and not paths
and \
973 event
.button
== self
.context_menu_mouse_button
:
974 # No selection or clicked outside selection;
975 # select the single item where we clicked
976 treeview
.grab_focus()
977 treeview
.set_cursor(path
, column
, 0)
981 # Unselect any remaining items (clicked elsewhere)
982 if hasattr(treeview
, 'is_rubber_banding_active'):
983 if not treeview
.is_rubber_banding_active():
984 selection
.unselect_all()
986 selection
.unselect_all()
990 def downloads_list_get_selection(self
, model
=None, paths
=None):
991 if model
is None and paths
is None:
992 selection
= self
.treeDownloads
.get_selection()
993 model
, paths
= selection
.get_selected_rows()
995 can_queue
, can_cancel
, can_pause
, can_remove
= (True,)*4
996 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
997 model
.get_value(model
.get_iter(path
), \
998 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1000 for row_reference
, task
in selected_tasks
:
1001 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1002 download
.DownloadTask
.FAILED
, \
1003 download
.DownloadTask
.CANCELLED
):
1005 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1006 download
.DownloadTask
.QUEUED
, \
1007 download
.DownloadTask
.DOWNLOADING
):
1009 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1010 download
.DownloadTask
.DOWNLOADING
):
1012 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1013 download
.DownloadTask
.FAILED
, \
1014 download
.DownloadTask
.DONE
):
1017 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
1019 def _for_each_task_set_status(self
, tasks
, status
):
1020 episode_urls
= set()
1021 model
= self
.treeDownloads
.get_model()
1022 for row_reference
, task
in tasks
:
1023 if status
== download
.DownloadTask
.QUEUED
:
1024 # Only queue task when its paused/failed/cancelled
1025 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
):
1026 self
.download_queue_manager
.add_task(task
)
1027 self
.enable_download_list_update()
1028 elif status
== download
.DownloadTask
.CANCELLED
:
1029 # Cancelling a download allowed when downloading/queued
1030 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1031 task
.status
= status
1032 # Cancelling paused downloads requires a call to .run()
1033 elif task
.status
== task
.PAUSED
:
1034 task
.status
= status
1035 # Call run, so the partial file gets deleted
1037 elif status
== download
.DownloadTask
.PAUSED
:
1038 # Pausing a download only when queued/downloading
1039 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1040 task
.status
= status
1041 elif status
is None:
1042 # Remove the selected task - cancel downloading/queued tasks
1043 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1044 task
.status
= task
.CANCELLED
1045 model
.remove(model
.get_iter(row_reference
.get_path()))
1046 # Remember the URL, so we can tell the UI to update
1048 # We don't "see" this task anymore - remove it;
1049 # this is needed, so update_episode_list_icons()
1050 # below gets the correct list of "seen" tasks
1051 self
.download_tasks_seen
.remove(task
)
1052 except KeyError, key_error
:
1053 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1054 episode_urls
.add(task
.url
)
1055 # Tell the task that it has been removed (so it can clean up)
1056 task
.removed_from_list()
1058 # We can (hopefully) simply set the task status here
1059 task
.status
= status
1060 # Tell the podcasts tab to update icons for our removed podcasts
1061 self
.update_episode_list_icons(episode_urls
)
1062 # Update the tab title and downloads list
1063 self
.update_downloads_list()
1065 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1066 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1068 if not hasattr(treeview
, 'is_rubber_banding_active'):
1071 return not treeview
.is_rubber_banding_active()
1073 if event
.button
== self
.context_menu_mouse_button
:
1074 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
= \
1075 self
.downloads_list_get_selection(model
, paths
)
1077 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
):
1078 # This creates a menu item for selection-wide actions
1079 item
= gtk
.ImageMenuItem(label
)
1080 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1081 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
))
1082 item
.set_sensitive(sensitive
)
1083 return self
.set_finger_friendly(item
)
1087 item
= gtk
.ImageMenuItem(_('Episode details'))
1088 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1089 if len(selected_tasks
) == 1:
1090 row_reference
, task
= selected_tasks
[0]
1091 episode
= task
.episode
1092 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1094 item
.set_sensitive(False)
1095 menu
.append(self
.set_finger_friendly(item
))
1096 menu
.append(gtk
.SeparatorMenuItem())
1097 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
))
1098 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1099 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1100 menu
.append(gtk
.SeparatorMenuItem())
1101 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1103 if gpodder
.ui
.maemo
:
1104 # Because we open the popup on left-click for Maemo,
1105 # we also include a non-action to close the menu
1106 menu
.append(gtk
.SeparatorMenuItem())
1107 item
= gtk
.ImageMenuItem(_('Close this menu'))
1108 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1110 menu
.append(self
.set_finger_friendly(item
))
1113 menu
.popup(None, None, None, event
.button
, event
.time
)
1116 def treeview_channels_show_context_menu(self
, treeview
, event
):
1117 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1121 if event
.button
== 3:
1126 item
= gtk
.ImageMenuItem( _('Open download folder'))
1127 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1128 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1131 item
= gtk
.ImageMenuItem( _('Update Feed'))
1132 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1133 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1134 item
.set_sensitive( not self
.updating_feed_cache
)
1137 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1138 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1139 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1142 if self
.active_channel
.link
:
1143 item
= gtk
.ImageMenuItem(_('Visit website'))
1144 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1145 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1148 if self
.active_channel
.channel_is_locked
:
1149 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1150 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1151 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1152 menu
.append(self
.set_finger_friendly(item
))
1154 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1155 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1156 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1157 menu
.append(self
.set_finger_friendly(item
))
1160 menu
.append( gtk
.SeparatorMenuItem())
1162 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1163 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1166 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1167 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1171 # Disable tooltips while we are showing the menu, so
1172 # the tooltip will not appear over the menu
1173 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1174 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1175 menu
.popup( None, None, None, event
.button
, event
.time
)
1179 def on_itemClose_activate(self
, widget
):
1180 if self
.tray_icon
is not None:
1181 self
.iconify_main_window()
1183 self
.on_gPodder_delete_event(widget
)
1185 def cover_file_removed(self
, channel_url
):
1187 The Cover Downloader calls this when a previously-
1188 available cover has been removed from the disk. We
1189 have to update our model to reflect this change.
1191 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1193 def cover_download_finished(self
, channel_url
, pixbuf
):
1195 The Cover Downloader calls this when it has finished
1196 downloading (or registering, if already downloaded)
1197 a new channel cover, which is ready for displaying.
1199 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1201 def save_episode_as_file(self
, episode
):
1202 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1203 if episode
.was_downloaded(and_exists
=True):
1204 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1205 copy_from
= episode
.local_filename(create
=False)
1206 assert copy_from
is not None
1207 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1208 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1209 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1211 def copy_episodes_bluetooth(self
, episodes
):
1212 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1214 def convert_and_send_thread(episode
):
1215 for episode
in episodes
:
1216 filename
= episode
.local_filename(create
=False)
1217 assert filename
is not None
1218 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1219 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1220 (base
, ext
) = os
.path
.splitext(filename
)
1221 if not destfile
.endswith(ext
):
1225 shutil
.copyfile(filename
, destfile
)
1226 util
.bluetooth_send_file(destfile
)
1228 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1229 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1231 util
.delete_file(destfile
)
1233 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1235 def get_device_name(self
):
1236 if self
.config
.device_type
== 'ipod':
1238 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1239 return _('MP3 player')
1241 return '(unknown device)'
1243 def _treeview_button_released(self
, treeview
, event
):
1244 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1245 dy
= int(abs(event
.y
-ypos
))
1246 dx
= int(event
.x
-xpos
)
1248 selection
= treeview
.get_selection()
1249 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1250 if path
is None or dy
> 30:
1251 return (False, dx
, dy
)
1253 path
, column
, x
, y
= path
1254 selection
.select_path(path
)
1255 treeview
.set_cursor(path
)
1256 treeview
.grab_focus()
1258 return (True, dx
, dy
)
1260 def treeview_channels_handle_gestures(self
, treeview
, event
):
1261 if self
.currently_updating
:
1264 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1267 if self
.config
.maemo_enable_gestures
:
1269 self
.on_itemUpdateChannel_activate()
1271 self
.on_itemEditChannel_activate(treeview
)
1275 def treeview_available_handle_gestures(self
, treeview
, event
):
1276 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1279 if self
.config
.maemo_enable_gestures
:
1281 self
.on_playback_selected_episodes(None)
1284 self
.on_shownotes_selected_episodes(None)
1287 # Pass the event to the context menu handler for treeAvailable
1288 self
.treeview_available_show_context_menu(treeview
, event
)
1292 def treeview_available_show_context_menu(self
, treeview
, event
):
1293 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1295 if not hasattr(treeview
, 'is_rubber_banding_active'):
1298 return not treeview
.is_rubber_banding_active()
1300 if event
.button
== self
.context_menu_mouse_button
:
1301 episodes
= self
.get_selected_episodes()
1302 any_locked
= any(e
.is_locked
for e
in episodes
)
1303 any_played
= any(e
.is_played
for e
in episodes
)
1304 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1308 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1310 if open_instead_of_play
:
1311 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1313 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1315 item
.set_sensitive(can_play
)
1316 item
.connect('activate', self
.on_playback_selected_episodes
)
1317 menu
.append(self
.set_finger_friendly(item
))
1320 item
= gtk
.ImageMenuItem(_('Download'))
1321 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1322 item
.set_sensitive(can_download
)
1323 item
.connect('activate', self
.on_download_selected_episodes
)
1324 menu
.append(self
.set_finger_friendly(item
))
1326 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1327 item
.connect('activate', self
.on_item_cancel_download_activate
)
1328 menu
.append(self
.set_finger_friendly(item
))
1330 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1331 item
.set_sensitive(can_delete
)
1332 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1333 menu
.append(self
.set_finger_friendly(item
))
1336 item
= gtk
.ImageMenuItem(_('Do not download'))
1337 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1338 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1339 menu
.append(self
.set_finger_friendly(item
))
1341 item
= gtk
.ImageMenuItem(_('Mark as new'))
1342 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
1343 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1344 menu
.append(self
.set_finger_friendly(item
))
1348 # Ok, this probably makes sense to only display for downloaded files
1349 if can_play
and not can_download
:
1350 menu
.append( gtk
.SeparatorMenuItem())
1351 item
= gtk
.ImageMenuItem(_('Save to disk'))
1352 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
1353 item
.connect('activate', lambda w
: [self
.save_episode_as_file(e
) for e
in episodes
])
1354 menu
.append(self
.set_finger_friendly(item
))
1355 if self
.bluetooth_available
:
1356 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
1357 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1358 item
.connect('activate', lambda w
: self
.copy_episodes_bluetooth(episodes
))
1359 menu
.append(self
.set_finger_friendly(item
))
1361 item
= gtk
.ImageMenuItem(_('Transfer to %s') % self
.get_device_name())
1362 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1363 item
.connect('activate', lambda w
: self
.on_sync_to_ipod_activate(w
, episodes
))
1364 menu
.append(self
.set_finger_friendly(item
))
1367 menu
.append( gtk
.SeparatorMenuItem())
1369 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
1370 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
1371 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
1372 menu
.append(self
.set_finger_friendly(item
))
1374 item
= gtk
.ImageMenuItem(_('Mark as played'))
1375 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
1376 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
1377 menu
.append(self
.set_finger_friendly(item
))
1380 item
= gtk
.ImageMenuItem(_('Allow deletion'))
1381 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1382 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, False))
1383 menu
.append(self
.set_finger_friendly(item
))
1385 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
1386 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1387 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, True))
1388 menu
.append(self
.set_finger_friendly(item
))
1390 menu
.append(gtk
.SeparatorMenuItem())
1391 # Single item, add episode information menu item
1392 item
= gtk
.ImageMenuItem(_('Episode details'))
1393 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1394 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1395 menu
.append(self
.set_finger_friendly(item
))
1397 # If we have it, also add episode website link
1398 if episodes
[0].link
and episodes
[0].link
!= episodes
[0].url
:
1399 item
= gtk
.ImageMenuItem(_('Visit website'))
1400 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1401 item
.connect('activate', lambda w
: util
.open_website(episodes
[0].link
))
1402 menu
.append(self
.set_finger_friendly(item
))
1404 if gpodder
.ui
.maemo
:
1405 # Because we open the popup on left-click for Maemo,
1406 # we also include a non-action to close the menu
1407 menu
.append(gtk
.SeparatorMenuItem())
1408 item
= gtk
.ImageMenuItem(_('Close this menu'))
1409 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1410 menu
.append(self
.set_finger_friendly(item
))
1413 # Disable tooltips while we are showing the menu, so
1414 # the tooltip will not appear over the menu
1415 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1416 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1417 menu
.popup( None, None, None, event
.button
, event
.time
)
1421 def set_title(self
, new_title
):
1422 if not gpodder
.ui
.fremantle
:
1423 self
.default_title
= new_title
1424 self
.gPodder
.set_title(new_title
)
1426 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1428 Updates the status icons in the episode list.
1430 If urls is given, it should be a list of URLs
1431 of episodes that should be updated.
1433 If urls is None, set ONE OF selected, all to
1434 True (the former updates just the selected
1435 episodes and the latter updates all episodes).
1437 if urls
is not None:
1438 # We have a list of URLs to walk through
1439 self
.episode_list_model
.update_by_urls(urls
, \
1440 self
.episode_is_downloading
, \
1441 self
.config
.episode_list_descriptions
and \
1443 elif selected
and not all
:
1444 # We should update all selected episodes
1445 selection
= self
.treeAvailable
.get_selection()
1446 model
, paths
= selection
.get_selected_rows()
1447 for path
in reversed(paths
):
1448 iter = model
.get_iter(path
)
1449 self
.episode_list_model
.update_by_filter_iter(iter, \
1450 self
.episode_is_downloading
, \
1451 self
.config
.episode_list_descriptions
and \
1453 elif all
and not selected
:
1454 # We update all (even the filter-hidden) episodes
1455 self
.episode_list_model
.update_all(\
1456 self
.episode_is_downloading
, \
1457 self
.config
.episode_list_descriptions
and \
1460 # Wrong/invalid call - have to specify at least one parameter
1461 raise ValueError('Invalid call to update_episode_list_icons')
1463 def episode_list_status_changed(self
, episodes
):
1464 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1465 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1468 def clean_up_downloads(self
, delete_partial
=False):
1469 # Clean up temporary files left behind by old gPodder versions
1470 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
1473 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
1475 for tempfile
in temporary_files
:
1476 util
.delete_file(tempfile
)
1478 # Clean up empty download folders and abandoned download folders
1479 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
1480 for ddir
in download_dirs
:
1481 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1482 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
1483 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
1484 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
1485 shutil
.rmtree(ddir
, ignore_errors
=True)
1487 def streaming_possible(self
):
1488 return self
.config
.player
and \
1489 self
.config
.player
!= 'default' and \
1492 def playback_episodes_for_real(self
, episodes
):
1493 groups
= collections
.defaultdict(list)
1494 for episode
in episodes
:
1495 file_type
= episode
.file_type()
1496 if file_type
== 'video' and self
.config
.videoplayer
and \
1497 self
.config
.videoplayer
!= 'default':
1498 player
= self
.config
.videoplayer
1499 if gpodder
.ui
.diablo
:
1500 # Use the wrapper script if it's installed to crop 3GP YouTube
1501 # videos to fit the screen (looks much nicer than w/ black border)
1502 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
1503 player
= 'gpodder-mplayer'
1504 elif file_type
== 'audio' and self
.config
.player
and \
1505 self
.config
.player
!= 'default':
1506 player
= self
.config
.player
1510 if file_type
not in ('audio', 'video') or \
1511 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
1512 (file_type
== 'video' and not self
.config
.video_played_dbus
):
1513 # Mark episode as played in the database
1514 episode
.mark(is_played
=True)
1516 filename
= episode
.local_filename(create
=False)
1517 if filename
is None or not os
.path
.exists(filename
):
1518 filename
= episode
.url
1519 groups
[player
].append(filename
)
1521 # Open episodes with system default player
1522 if 'default' in groups
:
1523 for filename
in groups
['default']:
1524 log('Opening with system default: %s', filename
, sender
=self
)
1525 util
.gui_open(filename
)
1526 del groups
['default']
1527 elif gpodder
.ui
.maemo
:
1528 # When on Maemo and not opening with default, show a notification
1529 # (no startup notification for Panucci / MPlayer yet...)
1530 if len(episodes
) == 1:
1531 text
= _('Opening %s') % episodes
[0].title
1533 text
= _('Opening %d episodes') % len(episodes
)
1535 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, text
)
1537 def destroy_banner_later(banner
):
1540 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
1542 # For each type now, go and create play commands
1543 for group
in groups
:
1544 for command
in util
.format_desktop_command(group
, groups
[group
]):
1545 log('Executing: %s', repr(command
), sender
=self
)
1546 subprocess
.Popen(command
)
1548 def playback_episodes(self
, episodes
):
1549 episodes
= [e
for e
in episodes
if \
1550 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()]
1553 self
.playback_episodes_for_real(episodes
)
1554 except Exception, e
:
1555 log('Error in playback!', sender
=self
, traceback
=True)
1556 self
.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget
=self
.toolPreferences
)
1558 channel_urls
= set()
1559 episode_urls
= set()
1560 for episode
in episodes
:
1561 channel_urls
.add(episode
.channel
.url
)
1562 episode_urls
.add(episode
.url
)
1563 self
.update_episode_list_icons(episode_urls
)
1564 self
.update_podcast_list_model(channel_urls
)
1566 def play_or_download(self
):
1567 if not gpodder
.ui
.fremantle
:
1568 if self
.wNotebook
.get_current_page() > 0:
1569 if gpodder
.ui
.desktop
:
1570 self
.toolCancel
.set_sensitive(True)
1573 if self
.currently_updating
:
1574 return (False, False, False, False, False, False)
1576 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1577 ( is_played
, is_locked
) = (False,)*2
1579 open_instead_of_play
= False
1581 selection
= self
.treeAvailable
.get_selection()
1582 if selection
.count_selected_rows() > 0:
1583 (model
, paths
) = selection
.get_selected_rows()
1586 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
1588 if episode
.file_type() not in ('audio', 'video'):
1589 open_instead_of_play
= True
1591 if episode
.was_downloaded():
1592 can_play
= episode
.was_downloaded(and_exists
=True)
1594 is_played
= episode
.is_played
1595 is_locked
= episode
.is_locked
1599 if self
.episode_is_downloading(episode
):
1604 can_download
= can_download
and not can_cancel
1605 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
1606 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
1608 if gpodder
.ui
.desktop
:
1609 if open_instead_of_play
:
1610 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
1612 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
1613 self
.toolPlay
.set_sensitive( can_play
)
1614 self
.toolDownload
.set_sensitive( can_download
)
1615 self
.toolTransfer
.set_sensitive( can_transfer
)
1616 self
.toolCancel
.set_sensitive( can_cancel
)
1618 if not gpodder
.ui
.fremantle
:
1619 self
.item_cancel_download
.set_sensitive(can_cancel
)
1620 self
.itemDownloadSelected
.set_sensitive(can_download
)
1621 self
.itemOpenSelected
.set_sensitive(can_play
)
1622 self
.itemPlaySelected
.set_sensitive(can_play
)
1623 self
.itemDeleteSelected
.set_sensitive(can_play
and not can_download
)
1624 self
.item_toggle_played
.set_sensitive(can_play
)
1625 self
.item_toggle_lock
.set_sensitive(can_play
)
1626 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
1627 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
1629 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
1631 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1632 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1634 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1635 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1637 def episode_new_status_changed(self
, urls
):
1638 self
.update_podcast_list_model()
1639 self
.update_episode_list_icons(urls
)
1641 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
1642 """Update the podcast list treeview model
1644 If urls is given, it should list the URLs of each
1645 podcast that has to be updated in the list.
1647 If selected is True, only update the model contents
1648 for the currently-selected podcast - nothing more.
1650 The caller can optionally specify "select_url",
1651 which is the URL of the podcast that is to be
1652 selected in the list after the update is complete.
1653 This only works if the podcast list has to be
1654 reloaded; i.e. something has been added or removed
1655 since the last update of the podcast list).
1657 selection
= self
.treeChannels
.get_selection()
1658 model
, iter = selection
.get_selected()
1661 # very cheap! only update selected channel
1662 if iter is not None:
1663 self
.podcast_list_model
.update_by_filter_iter(iter)
1664 elif not self
.channel_list_changed
:
1665 # we can keep the model, but have to update some
1667 # still cheaper than reloading the whole list
1668 self
.podcast_list_model
.update_all()
1670 # ok, we got a bunch of urls to update
1671 self
.podcast_list_model
.update_by_urls(urls
)
1673 if model
and iter and select_url
is None:
1674 # Get the URL of the currently-selected podcast
1675 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
1677 # Update the podcast list model with new channels
1678 self
.podcast_list_model
.set_channels(self
.channels
)
1681 selected_iter
= model
.get_iter_first()
1682 # Find the previously-selected URL in the new
1683 # model if we have an URL (else select first)
1684 if select_url
is not None:
1685 pos
= model
.get_iter_first()
1686 while pos
is not None:
1687 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
1688 if url
== select_url
:
1691 pos
= model
.iter_next(pos
)
1693 if not gpodder
.ui
.fremantle
:
1694 if selected_iter
is not None:
1695 selection
.select_iter(selected_iter
)
1696 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1698 log('Cannot select podcast in list', traceback
=True, sender
=self
)
1699 self
.channel_list_changed
= False
1701 def episode_is_downloading(self
, episode
):
1702 """Returns True if the given episode is being downloaded at the moment"""
1706 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
1708 def update_episode_list_model(self
):
1709 if self
.channels
and self
.active_channel
is not None:
1710 if gpodder
.ui
.diablo
:
1711 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, _('Loading episodes'))
1715 if gpodder
.ui
.fremantle
:
1716 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
1718 self
.currently_updating
= True
1719 self
.episode_list_model
.clear()
1720 def do_update_episode_list_model():
1721 self
.episode_list_model
.add_from_channel(\
1722 self
.active_channel
, \
1723 self
.episode_is_downloading
, \
1724 self
.config
.episode_list_descriptions \
1725 and gpodder
.ui
.desktop
)
1727 def on_episode_list_model_updated():
1728 if banner
is not None:
1730 if gpodder
.ui
.fremantle
:
1731 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
1732 self
.treeAvailable
.columns_autosize()
1733 self
.currently_updating
= False
1734 self
.play_or_download()
1735 util
.idle_add(on_episode_list_model_updated
)
1736 threading
.Thread(target
=do_update_episode_list_model
).start()
1738 self
.episode_list_model
.clear()
1740 def offer_new_episodes(self
, channels
=None):
1741 new_episodes
= self
.get_new_episodes(channels
)
1743 self
.new_episodes_show(new_episodes
)
1747 def add_podcast_list(self
, urls
, auth_tokens
=None):
1748 """Subscribe to a list of podcast given their URLs
1750 If auth_tokens is given, it should be a dictionary
1751 mapping URLs to (username, password) tuples."""
1753 if auth_tokens
is None:
1756 # Sort and split the URL list into five buckets
1757 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
1758 for input_url
in urls
:
1759 url
= util
.normalize_feed_url(input_url
)
1761 # Fail this one because the URL is not valid
1762 failed
.append(input_url
)
1763 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
1764 # A podcast already exists in the list for this URL
1765 existing
.append(url
)
1767 # This URL has survived the first round - queue for add
1769 if url
!= input_url
and input_url
in auth_tokens
:
1770 auth_tokens
[url
] = auth_tokens
[input_url
]
1775 progress
= ProgressIndicator(_('Adding podcasts'), \
1776 _('Please wait while episode information is downloaded.'), \
1777 parent
=self
.main_window
)
1779 def on_after_update():
1780 progress
.on_finished()
1781 # Report already-existing subscriptions to the user
1783 title
= _('Existing subscriptions skipped')
1784 message
= _('You are already subscribed to these podcasts:') \
1785 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
1786 self
.show_message(message
, title
, widget
=self
.treeChannels
)
1788 # Report subscriptions that require authentication
1792 title
= _('Podcast requires authentication')
1793 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
1794 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
1796 retry_podcasts
[url
] = auth_tokens
1798 # Stop asking the user for more login data
1801 error_messages
[url
] = _('Authentication failed')
1805 # If we have authentication data to retry, do so here
1807 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
1809 # Report website redirections
1810 for url
in redirections
:
1811 title
= _('Website redirection detected')
1812 message
= _('The URL %s redirects to %s.') \
1813 + '\n\n' + _('Do you want to visit the website now?')
1814 message
= message
% (url
, redirections
[url
])
1815 if self
.show_confirmation(message
, title
):
1816 util
.open_website(url
)
1820 # Report failed subscriptions to the user
1822 title
= _('Could not add some podcasts')
1823 message
= _('Some podcasts could not be added to your list:') \
1824 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
1825 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
1826 self
.show_message(message
, title
, important
=True)
1828 # If at least one podcast has been added, save and update all
1829 if self
.channel_list_changed
:
1830 self
.save_channels_opml()
1832 # If only one podcast was added, select it after the update
1833 if len(worked
) == 1:
1838 # Update the list of subscribed podcasts
1839 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
1840 self
.update_podcasts_tab()
1842 # Offer to download new episodes
1843 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
1846 # After the initial sorting and splitting, try all queued podcasts
1847 length
= len(queued
)
1848 for index
, url
in enumerate(queued
):
1849 progress
.on_progress(float(index
)/float(length
))
1850 progress
.on_message(url
)
1851 log('QUEUE RUNNER: %s', url
, sender
=self
)
1853 # The URL is valid and does not exist already - subscribe!
1854 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
1855 authentication_tokens
=auth_tokens
.get(url
, None), \
1856 max_episodes
=self
.config
.max_episodes_per_feed
, \
1857 download_dir
=self
.config
.download_dir
)
1860 username
, password
= util
.username_password_from_url(url
)
1861 except ValueError, ve
:
1862 username
, password
= (None, None)
1864 if username
is not None and channel
.username
is None and \
1865 password
is not None and channel
.password
is None:
1866 channel
.username
= username
1867 channel
.password
= password
1870 self
._update
_cover
(channel
)
1871 except feedcore
.AuthenticationRequired
:
1872 if url
in auth_tokens
:
1873 # Fail for wrong authentication data
1874 error_messages
[url
] = _('Authentication failed')
1877 # Queue for login dialog later
1880 except feedcore
.WifiLogin
, error
:
1881 redirections
[url
] = error
.data
1883 error_messages
[url
] = _('Redirection detected')
1885 except Exception, e
:
1886 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
1887 error_messages
[url
] = str(e
)
1891 assert channel
is not None
1892 worked
.append(channel
.url
)
1893 self
.channels
.append(channel
)
1894 self
.channel_list_changed
= True
1895 util
.idle_add(on_after_update
)
1896 threading
.Thread(target
=thread_proc
).start()
1898 def save_channels_opml(self
):
1899 exporter
= opml
.Exporter(gpodder
.subscription_file
)
1900 return exporter
.write(self
.channels
)
1902 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
1904 self
.updating_feed_cache
= False
1906 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
1907 self
.channel_list_changed
= True
1908 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
1910 # Only search for new episodes in podcasts that have been
1911 # updated, not in other podcasts (for single-feed updates)
1912 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
1914 if gpodder
.ui
.fremantle
:
1915 self
.button_subscribe
.set_sensitive(True)
1916 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1917 self
.update_podcasts_tab()
1919 self
.new_episodes_show(episodes
)
1921 self
.show_message(_('No new episodes. Please check for new episodes later.'), important
=True)
1925 self
.tray_icon
.set_status()
1927 if self
.feed_cache_update_cancelled
:
1928 # The user decided to abort the feed update
1929 self
.show_update_feeds_buttons()
1931 # Nothing new here - but inform the user
1932 self
.pbFeedUpdate
.set_fraction(1.0)
1933 self
.pbFeedUpdate
.set_text(_('No new episodes'))
1934 self
.feed_cache_update_cancelled
= True
1935 self
.btnCancelFeedUpdate
.show()
1936 self
.btnCancelFeedUpdate
.set_sensitive(True)
1937 if gpodder
.ui
.maemo
:
1938 # btnCancelFeedUpdate is a ToolButton on Maemo
1939 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
1941 # btnCancelFeedUpdate is a normal gtk.Button
1942 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
1944 # New episodes are available
1945 self
.pbFeedUpdate
.set_fraction(1.0)
1946 # Are we minimized and should we auto download?
1947 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
1948 self
.download_episode_list(episodes
)
1949 if len(episodes
) == 1:
1950 title
= _('Downloading one new episode.')
1952 title
= _('Downloading %d new episodes.') % len(episodes
)
1954 if not gpodder
.ui
.fremantle
:
1955 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
1956 self
.show_update_feeds_buttons()
1958 self
.show_update_feeds_buttons()
1959 # New episodes are available and we are not minimized
1960 if not self
.config
.do_not_show_new_episodes_dialog
:
1961 self
.new_episodes_show(episodes
, notification
=True)
1963 if len(episodes
) == 1:
1964 message
= _('One new episode is available for download')
1966 message
= _('%i new episodes are available for download' % len(episodes
))
1968 self
.pbFeedUpdate
.set_text(message
)
1970 def _update_cover(self
, channel
):
1971 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
1972 self
.cover_downloader
.request_cover(channel
)
1974 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
1975 total
= len(channels
)
1977 for updated
, channel
in enumerate(channels
):
1978 if not self
.feed_cache_update_cancelled
:
1980 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1981 if channel
.query_automatic_update() or total
== 1 or not self
.config
.feed_update_skipping
:
1982 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
1984 log('Skipping update of %s (see feed_update_skipping)', channel
.title
, sender
=self
)
1985 self
._update
_cover
(channel
)
1986 except Exception, e
:
1987 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
)
1988 log('Error: %s', str(e
), sender
=self
, traceback
=True)
1990 if self
.feed_cache_update_cancelled
:
1993 if gpodder
.ui
.fremantle
:
1994 self
.button_podcasts
.set_value(_('%d/%d updated') % (updated
, total
))
1997 # By the time we get here the update may have already been cancelled
1998 if not self
.feed_cache_update_cancelled
:
1999 def update_progress():
2000 progression
= _('Updated %s (%d/%d)') % (channel
.title
, updated
, total
)
2001 self
.pbFeedUpdate
.set_text(progression
)
2003 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2004 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2005 util
.idle_add(update_progress
)
2007 updated_urls
= [c
.url
for c
in channels
]
2008 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2010 def show_update_feeds_buttons(self
):
2011 # Make sure that the buttons for updating feeds
2012 # appear - this should happen after a feed update
2013 if gpodder
.ui
.maemo
:
2014 self
.btnUpdateSelectedFeed
.show()
2015 self
.toolFeedUpdateProgress
.hide()
2016 self
.btnCancelFeedUpdate
.hide()
2017 self
.btnCancelFeedUpdate
.set_is_important(False)
2018 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2019 self
.toolbarSpacer
.set_expand(True)
2020 self
.toolbarSpacer
.set_draw(False)
2022 self
.hboxUpdateFeeds
.hide()
2023 self
.btnUpdateFeeds
.show()
2024 self
.itemUpdate
.set_sensitive(True)
2025 self
.itemUpdateChannel
.set_sensitive(True)
2027 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2028 if not self
.feed_cache_update_cancelled
:
2029 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2030 self
.feed_cache_update_cancelled
= True
2031 self
.btnCancelFeedUpdate
.set_sensitive(False)
2033 self
.show_update_feeds_buttons()
2035 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2036 if self
.updating_feed_cache
:
2039 if not force_update
:
2040 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2041 self
.channel_list_changed
= True
2042 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2045 self
.updating_feed_cache
= True
2047 if channels
is None:
2048 channels
= self
.channels
2050 if gpodder
.ui
.fremantle
:
2051 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2052 hildon
.hildon_banner_show_information(self
.main_window
, \
2053 '', _('Updating podcast feeds'))
2054 self
.button_subscribe
.set_sensitive(False)
2056 self
.itemUpdate
.set_sensitive(False)
2057 self
.itemUpdateChannel
.set_sensitive(False)
2060 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2062 if len(channels
) == 1:
2063 text
= _('Updating "%s"...') % channels
[0].title
2065 text
= _('Updating %d feeds...') % len(channels
)
2066 self
.pbFeedUpdate
.set_text(text
)
2067 self
.pbFeedUpdate
.set_fraction(0)
2069 self
.feed_cache_update_cancelled
= False
2070 self
.btnCancelFeedUpdate
.show()
2071 self
.btnCancelFeedUpdate
.set_sensitive(True)
2072 if gpodder
.ui
.maemo
:
2073 self
.toolbarSpacer
.set_expand(False)
2074 self
.toolbarSpacer
.set_draw(True)
2075 self
.btnUpdateSelectedFeed
.hide()
2076 self
.toolFeedUpdateProgress
.show_all()
2078 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2079 self
.hboxUpdateFeeds
.show_all()
2080 self
.btnUpdateFeeds
.hide()
2082 args
= (channels
, select_url_afterwards
)
2083 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2085 def on_gPodder_delete_event(self
, widget
, *args
):
2086 """Called when the GUI wants to close the window
2087 Displays a confirmation dialog (and closes/hides gPodder)
2090 downloading
= self
.download_status_model
.are_downloads_in_progress()
2092 # Only iconify if we are using the window's "X" button,
2093 # but not when we are using "Quit" in the menu or toolbar
2094 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'):
2095 self
.iconify_main_window()
2096 elif self
.config
.on_quit_ask
or downloading
:
2097 if gpodder
.ui
.maemo
:
2098 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2100 self
.close_gpodder()
2103 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2104 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2105 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2107 title
= _('Quit gPodder')
2109 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2111 message
= _('Do you really want to quit gPodder now?')
2113 dialog
.set_title(title
)
2114 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2116 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2117 dialog
.vbox
.pack_start(cb_ask
)
2120 quit_button
.grab_focus()
2121 result
= dialog
.run()
2124 if result
== gtk
.RESPONSE_CLOSE
:
2125 if not downloading
and cb_ask
.get_active() == True:
2126 self
.config
.on_quit_ask
= False
2127 self
.close_gpodder()
2129 self
.close_gpodder()
2133 def close_gpodder(self
):
2134 """ clean everything and exit properly
2137 if self
.save_channels_opml():
2138 if self
.config
.my_gpodder_autoupload
:
2139 log('Uploading to my.gpodder.org on close', sender
=self
)
2140 util
.idle_add(self
.on_upload_to_mygpo
, None)
2142 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2146 if self
.tray_icon
is not None:
2147 self
.tray_icon
.set_visible(False)
2149 # Notify all tasks to to carry out any clean-up actions
2150 self
.download_status_model
.tell_all_tasks_to_quit()
2152 while gtk
.events_pending():
2153 gtk
.main_iteration(False)
2160 def get_old_episodes(self
):
2162 for channel
in self
.channels
:
2163 for episode
in channel
.get_downloaded_episodes():
2164 if episode
.age_in_days() > self
.config
.episode_old_age
and \
2165 not episode
.is_locked
and episode
.is_played
:
2166 episodes
.append(episode
)
2169 def delete_episode_list(self
, episodes
, confirm
=True):
2173 count
= len(episodes
)
2176 episode
= episodes
[0]
2177 if episode
.is_locked
:
2178 title
= _('%s is locked') % saxutils
.escape(episode
.title
)
2179 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2180 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2183 title
= _('Remove %s?') % saxutils
.escape(episode
.title
)
2184 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.")
2186 title
= _('Remove %d episodes?') % count
2187 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.')
2189 locked_count
= sum(int(e
.is_locked
) for e
in episodes
if e
.is_locked
is not None)
2191 if count
== locked_count
:
2192 title
= _('Episodes are locked')
2193 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2194 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2196 elif locked_count
> 0:
2197 title
= _('Remove %d out of %d episodes?') % (count
-locked_count
, count
)
2198 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.')
2200 if confirm
and not self
.show_confirmation(message
, title
):
2203 episode_urls
= set()
2204 channel_urls
= set()
2205 for episode
in episodes
:
2206 if episode
.is_locked
:
2207 log('Not deleting episode (is locked): %s', episode
.title
)
2209 log('Deleting episode: %s', episode
.title
)
2210 episode
.delete_from_disk()
2211 episode_urls
.add(episode
.url
)
2212 channel_urls
.add(episode
.channel
.url
)
2214 # Tell the shownotes window that we have removed the episode
2215 if self
.episode_shownotes_window
is not None and \
2216 self
.episode_shownotes_window
.episode
is not None and \
2217 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2218 self
.episode_shownotes_window
._download
_status
_changed
(None)
2220 # Episodes have been deleted - persist the database
2223 self
.update_episode_list_icons(episode_urls
)
2224 self
.update_podcast_list_model(channel_urls
)
2225 self
.play_or_download()
2228 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2229 if gpodder
.ui
.maemo
:
2231 ('maemo_remove_markup', None, None, _('Episode')),
2235 ('title_markup', None, None, _('Episode')),
2236 ('channel_prop', None, None, _('Podcast')),
2237 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2238 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2239 ('played_prop', None, None, _('Status')),
2240 ('age_prop', None, None, _('Downloaded')),
2243 selection_buttons
= {
2244 _('Select played'): lambda episode
: episode
.is_played
,
2245 _('Select older than %d days') % self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2248 instructions
= _('Select the episodes you want to delete:')
2252 for channel
in self
.channels
:
2253 for episode
in channel
.get_downloaded_episodes():
2254 if not episode
.is_locked
:
2255 episodes
.append(episode
)
2256 selected
.append(episode
.is_played
)
2258 gPodderEpisodeSelector(self
.gPodder
, title
= _('Remove old episodes'), instructions
= instructions
, \
2259 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2260 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2261 selection_buttons
= selection_buttons
, _config
=self
.config
)
2263 def on_selected_episodes_status_changed(self
):
2264 self
.update_episode_list_icons(selected
=True)
2265 self
.update_podcast_list_model(selected
=True)
2268 def mark_selected_episodes_new(self
):
2269 for episode
in self
.get_selected_episodes():
2271 self
.on_selected_episodes_status_changed()
2273 def mark_selected_episodes_old(self
):
2274 for episode
in self
.get_selected_episodes():
2276 self
.on_selected_episodes_status_changed()
2278 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2279 for episode
in self
.get_selected_episodes():
2281 episode
.mark(is_played
=not episode
.is_played
)
2283 episode
.mark(is_played
=new_value
)
2284 self
.on_selected_episodes_status_changed()
2286 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2287 for episode
in self
.get_selected_episodes():
2289 episode
.mark(is_locked
=not episode
.is_locked
)
2291 episode
.mark(is_locked
=new_value
)
2292 self
.on_selected_episodes_status_changed()
2294 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2295 if self
.active_channel
is None:
2298 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2299 self
.active_channel
.update_channel_lock()
2301 for episode
in self
.active_channel
.get_all_episodes():
2302 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
2304 self
.update_podcast_list_model(selected
=True)
2305 self
.update_episode_list_icons(all
=True)
2307 def on_itemUpdateChannel_activate(self
, widget
=None):
2308 if self
.active_channel
is None:
2309 title
= _('No podcast selected')
2310 message
= _('Please select a podcast in the podcasts list to update.')
2311 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2314 self
.update_feed_cache(channels
=[self
.active_channel
])
2316 def on_itemUpdate_activate(self
, widget
=None):
2318 self
.update_feed_cache()
2320 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
)
2322 def download_episode_list_paused(self
, episodes
):
2323 self
.download_episode_list(episodes
, True)
2325 def download_episode_list(self
, episodes
, add_paused
=False):
2326 for episode
in episodes
:
2327 log('Downloading episode: %s', episode
.title
, sender
= self
)
2328 if not episode
.was_downloaded(and_exists
=True):
2330 for task
in self
.download_tasks_seen
:
2331 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
2332 self
.download_queue_manager
.add_task(task
)
2333 self
.enable_download_list_update()
2341 task
= download
.DownloadTask(episode
, self
.config
)
2342 except Exception, e
:
2343 self
.show_message(_('Download error while downloading %s:\n\n%s') % (episode
.title
, str(e
)), _('Download error'), important
=True)
2344 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
2348 task
.status
= task
.PAUSED
2350 self
.download_queue_manager
.add_task(task
)
2352 self
.download_status_model
.register_task(task
)
2353 self
.enable_download_list_update()
2355 def cancel_task_list(self
, tasks
):
2360 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
2361 task
.status
= task
.CANCELLED
2362 elif task
.status
== task
.PAUSED
:
2363 task
.status
= task
.CANCELLED
2364 # Call run, so the partial file gets deleted
2367 self
.update_episode_list_icons([task
.url
for task
in tasks
])
2368 self
.play_or_download()
2370 # Update the tab title and downloads list
2371 self
.update_downloads_list()
2373 def new_episodes_show(self
, episodes
, notification
=False):
2374 if gpodder
.ui
.maemo
:
2376 ('maemo_markup', None, None, _('Episode')),
2378 show_notification
= notification
2381 ('title_markup', None, None, _('Episode')),
2382 ('channel_prop', None, None, _('Podcast')),
2383 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2384 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2386 show_notification
= False
2388 instructions
= _('Select the episodes you want to download:')
2390 if self
.new_episodes_window
is not None:
2391 self
.new_episodes_window
.main_window
.destroy()
2392 self
.new_episodes_window
= None
2394 def download_episodes_callback(episodes
):
2395 self
.new_episodes_window
= None
2396 self
.download_episode_list(episodes
)
2398 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
2399 title
=_('New episodes available'), \
2400 instructions
=instructions
, \
2401 episodes
=episodes
, \
2403 selected_default
=True, \
2404 stock_ok_button
= 'gpodder-download', \
2405 callback
=download_episodes_callback
, \
2406 remove_callback
=lambda e
: e
.mark_old(), \
2407 remove_action
=_('Mark as old'), \
2408 remove_finished
=self
.episode_new_status_changed
, \
2409 _config
=self
.config
, \
2410 show_notification
=show_notification
)
2412 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
2413 if not self
.offer_new_episodes():
2414 self
.show_message(_('Please check for new episodes later.'), \
2415 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
2417 def get_new_episodes(self
, channels
=None):
2418 if channels
is None:
2419 channels
= self
.channels
2421 for channel
in channels
:
2422 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
2423 episodes
.append(episode
)
2427 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
2428 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
2429 # The sync process might have updated the status of episodes,
2430 # therefore persist the database here to avoid losing data
2433 def on_cleanup_ipod_activate(self
, widget
, *args
):
2434 self
.sync_ui
.on_cleanup_device()
2436 def on_manage_device_playlist(self
, widget
):
2437 self
.sync_ui
.on_manage_device_playlist()
2439 def show_hide_tray_icon(self
):
2440 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
2441 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
2442 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
2443 self
.tray_icon
.set_visible(False)
2445 self
.tray_icon
= None
2447 if self
.config
.minimize_to_tray
and self
.tray_icon
:
2448 self
.tray_icon
.set_visible(self
.is_iconified())
2449 elif self
.tray_icon
:
2450 self
.tray_icon
.set_visible(True)
2452 def on_itemShowToolbar_activate(self
, widget
):
2453 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
2455 def on_itemShowDescription_activate(self
, widget
):
2456 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
2458 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
2459 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
2460 if self
.config
.podcast_list_hide_boring
:
2461 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2463 self
.podcast_list_model
.set_view_mode(-1)
2465 def on_item_view_podcasts_changed(self
, radioaction
, current
):
2467 if current
== self
.item_view_podcasts_all
:
2468 self
.podcast_list_model
.set_view_mode(-1)
2469 elif current
== self
.item_view_podcasts_downloaded
:
2470 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2471 elif current
== self
.item_view_podcasts_unplayed
:
2472 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2474 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
2476 def on_item_view_episodes_changed(self
, radioaction
, current
):
2477 if current
== self
.item_view_episodes_all
:
2478 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
2479 elif current
== self
.item_view_episodes_undeleted
:
2480 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
2481 elif current
== self
.item_view_episodes_downloaded
:
2482 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2483 elif current
== self
.item_view_episodes_unplayed
:
2484 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2486 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
2488 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
2489 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2491 def update_item_device( self
):
2492 if not gpodder
.ui
.fremantle
:
2493 if self
.config
.device_type
!= 'none':
2494 self
.itemDevice
.set_visible(True)
2495 self
.itemDevice
.label
= self
.get_device_name()
2497 self
.itemDevice
.set_visible(False)
2499 def properties_closed( self
):
2500 self
.show_hide_tray_icon()
2501 self
.update_item_device()
2502 if gpodder
.ui
.maemo
:
2503 selection
= self
.treeAvailable
.get_selection()
2504 if self
.config
.maemo_enable_gestures
or \
2505 self
.config
.enable_fingerscroll
:
2506 selection
.set_mode(gtk
.SELECTION_SINGLE
)
2508 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
2510 def on_itemPreferences_activate(self
, widget
, *args
):
2511 gPodderPreferences(self
.gPodder
, _config
=self
.config
, \
2512 callback_finished
=self
.properties_closed
, \
2513 user_apps_reader
=self
.user_apps_reader
)
2515 def on_itemDependencies_activate(self
, widget
):
2516 gPodderDependencyManager(self
.gPodder
)
2518 def require_my_gpodder_authentication(self
):
2519 if not self
.config
.my_gpodder_username
or not self
.config
.my_gpodder_password
:
2520 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'))
2521 if success
and authentication
[0] and authentication
[1]:
2522 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
= authentication
2529 def my_gpodder_offer_autoupload(self
):
2530 if not self
.config
.my_gpodder_autoupload
:
2531 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')):
2532 self
.config
.my_gpodder_autoupload
= True
2534 def on_download_from_mygpo(self
, widget
=None):
2535 if self
.require_my_gpodder_authentication():
2536 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2537 opml_data
= client
.download_subscriptions()
2538 if len(opml_data
) > 0:
2539 fp
= open(gpodder
.subscription_file
, 'w')
2542 (added
, skipped
) = (0, 0)
2543 i
= opml
.Importer(gpodder
.subscription_file
)
2545 existing
= [c
.url
for c
in self
.channels
]
2546 urls
= [item
['url'] for item
in i
.items
if item
['url'] not in existing
]
2548 skipped
= len(i
.items
) - len(urls
)
2551 self
.add_podcast_list(urls
)
2553 self
.my_gpodder_offer_autoupload()
2555 self
.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added
, skipped
), _('Result of subscription download'), widget
=self
.treeChannels
)
2556 elif widget
is not None:
2557 self
.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget
=self
.treeChannels
)
2559 self
.config
.my_gpodder_password
= ''
2560 self
.on_download_from_mygpo(widget
)
2562 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2564 def on_upload_to_mygpo(self
, widget
):
2565 if self
.require_my_gpodder_authentication():
2566 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2567 self
.save_channels_opml()
2568 success
, messages
= client
.upload_subscriptions(gpodder
.subscription_file
)
2569 if widget
is not None:
2571 self
.show_message('\n'.join(messages
), _('Results of upload'), important
=True)
2572 self
.config
.my_gpodder_password
= ''
2573 self
.on_upload_to_mygpo(widget
)
2575 self
.my_gpodder_offer_autoupload()
2576 self
.show_message('\n'.join(messages
), _('Results of upload'), widget
=self
.treeChannels
)
2578 log('Upload to my.gpodder.org failed, but widget is None!', sender
=self
)
2579 elif widget
is not None:
2580 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2582 def on_itemAddChannel_activate(self
, widget
=None):
2583 gPodderAddPodcast(self
.gPodder
, \
2584 add_urls_callback
=self
.add_podcast_list
)
2586 def on_itemEditChannel_activate(self
, widget
, *args
):
2587 if self
.active_channel
is None:
2588 title
= _('No podcast selected')
2589 message
= _('Please select a podcast in the podcasts list to edit.')
2590 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2593 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
2594 gPodderChannel(self
.main_window
, \
2595 channel
=self
.active_channel
, \
2596 callback_closed
=callback_closed
, \
2597 cover_downloader
=self
.cover_downloader
)
2599 def on_itemRemoveChannel_activate(self
, widget
, *args
):
2600 if self
.active_channel
is None:
2601 title
= _('No podcast selected')
2602 message
= _('Please select a podcast in the podcasts list to remove.')
2603 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2607 if gpodder
.ui
.desktop
:
2608 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2609 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
2610 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
2612 title
= _('Remove podcast and episodes?')
2613 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
2615 dialog
.set_title(title
)
2616 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2618 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
2619 dialog
.vbox
.pack_start(cb_ask
)
2621 result
= (dialog
.run() == gtk
.RESPONSE_YES
)
2622 keep_episodes
= cb_ask
.get_active()
2624 elif gpodder
.ui
.diablo
:
2625 result
= self
.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2626 keep_episodes
= False
2627 elif gpodder
.ui
.fremantle
:
2629 keep_episodes
= False
2632 # delete downloaded episodes only if checkbox is unchecked
2634 log('Not removing downloaded episodes', sender
=self
)
2636 self
.active_channel
.remove_downloaded()
2638 # Clean up downloads and download directories
2639 self
.clean_up_downloads()
2641 # cancel any active downloads from this channel
2642 for episode
in self
.active_channel
.get_all_episodes():
2643 self
.download_status_model
.cancel_by_url(episode
.url
)
2645 # get the URL of the podcast we want to select next
2646 position
= self
.channels
.index(self
.active_channel
)
2647 if position
== len(self
.channels
)-1:
2648 # this is the last podcast, so select the URL
2649 # of the item before this one (i.e. the "new last")
2650 select_url
= self
.channels
[position
-1].url
2652 # there is a podcast after the deleted one, so
2653 # we simply select the one that comes after it
2654 select_url
= self
.channels
[position
+1].url
2656 title
= self
.active_channel
.title
2658 # Remove the channel
2659 self
.active_channel
.delete(purge
=not keep_episodes
)
2660 self
.channels
.remove(self
.active_channel
)
2661 self
.channel_list_changed
= True
2662 self
.save_channels_opml()
2664 if gpodder
.ui
.fremantle
:
2665 self
.show_message(_('Podcast removed: %s') % title
)
2667 # Re-load the channels and select the desired new channel
2668 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
2670 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
2671 self
.update_podcasts_tab()
2673 def get_opml_filter(self
):
2674 filter = gtk
.FileFilter()
2675 filter.add_pattern('*.opml')
2676 filter.add_pattern('*.xml')
2677 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2680 def on_item_import_from_file_activate(self
, widget
, filename
=None):
2681 if filename
is None:
2682 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2683 # FIXME: Hildonization on Fremantle
2684 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2685 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2686 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2687 elif gpodder
.ui
.diablo
:
2688 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2689 dlg
.set_filter(self
.get_opml_filter())
2690 response
= dlg
.run()
2692 if response
== gtk
.RESPONSE_OK
:
2693 filename
= dlg
.get_filename()
2696 if filename
is not None:
2697 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
2698 custom_title
=_('Import podcasts from OPML file'), \
2699 add_urls_callback
=self
.add_podcast_list
, \
2700 hide_url_entry
=True)
2701 dir.download_opml_file(filename
)
2703 def on_itemExportChannels_activate(self
, widget
, *args
):
2704 if not self
.channels
:
2705 title
= _('Nothing to export')
2706 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2707 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2710 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2711 # FIXME: Hildonization on Fremantle
2712 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
2713 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2714 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
2715 elif gpodder
.ui
.diablo
:
2716 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
2717 dlg
.set_filter(self
.get_opml_filter())
2718 response
= dlg
.run()
2719 if response
== gtk
.RESPONSE_OK
:
2720 filename
= dlg
.get_filename()
2722 exporter
= opml
.Exporter( filename
)
2723 if exporter
.write(self
.channels
):
2724 if len(self
.channels
) == 1:
2725 title
= _('One subscription exported')
2727 title
= _('%d subscriptions exported') % len(self
.channels
)
2728 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
2730 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
2734 def on_itemImportChannels_activate(self
, widget
, *args
):
2735 if gpodder
.ui
.fremantle
:
2736 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
2737 self
.config
.toplist_url
, \
2738 self
.config
.opml_url
, \
2739 self
.add_podcast_list
, \
2740 self
.on_itemAddChannel_activate
, \
2741 self
.on_download_from_mygpo
, \
2742 self
.show_text_edit_dialog
)
2744 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
2745 add_urls_callback
=self
.add_podcast_list
)
2746 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
2748 def on_homepage_activate(self
, widget
, *args
):
2749 util
.open_website(gpodder
.__url
__)
2751 def on_wiki_activate(self
, widget
, *args
):
2752 util
.open_website('http://wiki.gpodder.org/')
2754 def on_bug_tracker_activate(self
, widget
, *args
):
2755 if gpodder
.ui
.maemo
:
2756 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2758 util
.open_website('http://bugs.gpodder.org/')
2760 def on_shop_activate(self
, widget
, *args
):
2761 util
.open_website('http://gpodder.org/shop')
2763 def on_wishlist_activate(self
, widget
, *args
):
2764 util
.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2766 def on_itemAbout_activate(self
, widget
, *args
):
2767 dlg
= gtk
.AboutDialog()
2768 dlg
.set_name('gPodder')
2769 dlg
.set_version(gpodder
.__version
__)
2770 dlg
.set_copyright(gpodder
.__copyright
__)
2771 dlg
.set_website(gpodder
.__url
__)
2772 dlg
.set_translator_credits( _('translator-credits'))
2773 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
2775 if gpodder
.ui
.desktop
:
2776 # For the "GUI" version, we add some more
2777 # items to the about dialog (credits and logo)
2780 'Thomas Perl <thpinfo.com>',
2783 if os
.path
.exists(gpodder
.credits_file
):
2784 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
2785 app_authors
+= ['', _('Patches, bug reports and donations by:')]
2786 app_authors
+= credits
2788 dlg
.set_authors(app_authors
)
2790 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
2792 dlg
.set_logo_icon_name('gpodder')
2793 elif gpodder
.ui
.fremantle
:
2794 for parent
in dlg
.vbox
.get_children():
2795 for child
in parent
.get_children():
2796 if isinstance(child
, gtk
.Label
):
2797 child
.set_selectable(False)
2801 def on_wNotebook_switch_page(self
, widget
, *args
):
2803 if gpodder
.ui
.maemo
:
2804 self
.tool_downloads
.set_active(page_num
== 1)
2805 page
= self
.wNotebook
.get_nth_page(page_num
)
2806 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2807 if page_num
== 0 and self
.active_channel
is not None:
2808 self
.set_title(self
.active_channel
.title
)
2810 self
.set_title(tab_label
)
2812 self
.play_or_download()
2813 self
.menuChannels
.set_sensitive(True)
2814 self
.menuSubscriptions
.set_sensitive(True)
2815 # The message area in the downloads tab should be hidden
2816 # when the user switches away from the downloads tab
2817 if self
.message_area
is not None:
2818 self
.message_area
.hide()
2819 self
.message_area
= None
2821 self
.menuChannels
.set_sensitive(False)
2822 self
.menuSubscriptions
.set_sensitive(False)
2823 if gpodder
.ui
.desktop
:
2824 self
.toolDownload
.set_sensitive(False)
2825 self
.toolPlay
.set_sensitive(False)
2826 self
.toolTransfer
.set_sensitive(False)
2827 self
.toolCancel
.set_sensitive(False)
2829 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
2830 # double-click action of the podcast list or enter
2831 self
.treeChannels
.set_cursor(path
)
2833 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2834 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2836 if model
is not None and iter is not None:
2837 old_active_channel
= self
.active_channel
2838 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
2840 if self
.active_channel
== old_active_channel
:
2843 if gpodder
.ui
.maemo
:
2844 self
.set_title(self
.active_channel
.title
)
2845 self
.itemEditChannel
.set_visible(True)
2846 self
.itemRemoveChannel
.set_visible(True)
2848 self
.active_channel
= None
2849 self
.itemEditChannel
.set_visible(False)
2850 self
.itemRemoveChannel
.set_visible(False)
2852 self
.update_episode_list_model()
2854 def on_btnEditChannel_clicked(self
, widget
, *args
):
2855 self
.on_itemEditChannel_activate( widget
, args
)
2857 def get_selected_episodes(self
):
2858 """Get a list of selected episodes from treeAvailable"""
2859 selection
= self
.treeAvailable
.get_selection()
2860 model
, paths
= selection
.get_selected_rows()
2862 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
2865 def on_transfer_selected_episodes(self
, widget
):
2866 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
2868 def on_playback_selected_episodes(self
, widget
):
2869 self
.playback_episodes(self
.get_selected_episodes())
2871 def on_shownotes_selected_episodes(self
, widget
):
2872 episodes
= self
.get_selected_episodes()
2874 episode
= episodes
.pop(0)
2875 self
.show_episode_shownotes(episode
)
2877 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
2879 def on_download_selected_episodes(self
, widget
):
2880 episodes
= self
.get_selected_episodes()
2881 self
.download_episode_list(episodes
)
2882 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
2883 self
.play_or_download()
2885 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
2886 """Double-click/enter action handler for treeAvailable"""
2887 # We should only have one one selected as it was double clicked!
2888 e
= self
.get_selected_episodes()[0]
2890 if (self
.config
.double_click_episode_action
== 'download'):
2891 # If the episode has already been downloaded and exists then play it
2892 if e
.was_downloaded(and_exists
=True):
2893 self
.playback_episodes(self
.get_selected_episodes())
2894 # else download it if it is not already downloading
2895 elif not self
.episode_is_downloading(e
):
2896 self
.download_episode_list([e
])
2897 self
.update_episode_list_icons([e
.url
])
2898 self
.play_or_download()
2899 elif (self
.config
.double_click_episode_action
== 'stream'):
2900 # If we happen to have downloaded this episode simple play it
2901 if e
.was_downloaded(and_exists
=True):
2902 self
.playback_episodes(self
.get_selected_episodes())
2903 # else if streaming is possible stream it
2904 elif self
.streaming_possible():
2905 self
.playback_episodes(self
.get_selected_episodes())
2907 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
2908 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
2910 # default action is to display show notes
2911 self
.on_shownotes_selected_episodes(widget
)
2913 def show_episode_shownotes(self
, episode
):
2914 if self
.episode_shownotes_window
is None:
2915 log('First-time use of episode window --- creating', sender
=self
)
2916 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
2917 _download_episode_list
=self
.download_episode_list
, \
2918 _playback_episodes
=self
.playback_episodes
, \
2919 _delete_episode_list
=self
.delete_episode_list
, \
2920 _episode_list_status_changed
=self
.episode_list_status_changed
, \
2921 _cancel_task_list
=self
.cancel_task_list
, \
2922 _episode_is_downloading
=self
.episode_is_downloading
)
2923 self
.episode_shownotes_window
.show(episode
)
2924 if self
.episode_is_downloading(episode
):
2925 self
.update_downloads_list()
2927 def auto_update_procedure(self
, first_run
=False):
2928 log('auto_update_procedure() got called', sender
=self
)
2929 if not first_run
and self
.config
.auto_update_feeds
and self
.is_iconified():
2930 self
.update_feed_cache(force_update
=True)
2932 next_update
= 60*1000*self
.config
.auto_update_frequency
2933 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2936 def on_treeDownloads_row_activated(self
, widget
, *args
):
2937 # Use the standard way of working on the treeview
2938 selection
= self
.treeDownloads
.get_selection()
2939 (model
, paths
) = selection
.get_selected_rows()
2940 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
2942 for tree_row_reference
, task
in selected_tasks
:
2943 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
2944 task
.status
= task
.PAUSED
2945 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
2946 self
.download_queue_manager
.add_task(task
)
2947 self
.enable_download_list_update()
2948 elif task
.status
== task
.DONE
:
2949 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
2951 self
.play_or_download()
2953 # Update the tab title and downloads list
2954 self
.update_downloads_list()
2956 def on_item_cancel_download_activate(self
, widget
):
2957 if self
.wNotebook
.get_current_page() == 0:
2958 selection
= self
.treeAvailable
.get_selection()
2959 (model
, paths
) = selection
.get_selected_rows()
2960 urls
= [model
.get_value(model
.get_iter(path
), \
2961 self
.episode_list_model
.C_URL
) for path
in paths
]
2962 selected_tasks
= [task
for task
in self
.download_tasks_seen \
2963 if task
.url
in urls
]
2965 selection
= self
.treeDownloads
.get_selection()
2966 (model
, paths
) = selection
.get_selected_rows()
2967 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
2968 self
.download_status_model
.C_TASK
) for path
in paths
]
2969 self
.cancel_task_list(selected_tasks
)
2971 def on_btnCancelAll_clicked(self
, widget
, *args
):
2972 self
.cancel_task_list(self
.download_tasks_seen
)
2974 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2975 if self
.wNotebook
.get_current_page() == 1:
2976 # Downloads tab visibile - skip (for now)
2979 episodes
= self
.get_selected_episodes()
2980 self
.delete_episode_list(episodes
)
2982 def on_key_press(self
, widget
, event
):
2983 # Allow tab switching with Ctrl + PgUp/PgDown
2984 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
2985 if event
.keyval
== gtk
.keysyms
.Page_Up
:
2986 self
.wNotebook
.prev_page()
2988 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
2989 self
.wNotebook
.next_page()
2992 # After this code we only handle Maemo hardware keys,
2993 # so if we are not a Maemo app, we don't do anything
2994 if not gpodder
.ui
.maemo
:
2998 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3000 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3003 if diff
!= 0 and not self
.currently_updating
:
3004 selection
= self
.treeChannels
.get_selection()
3005 (model
, iter) = selection
.get_selected()
3006 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3007 selection
.select_path(new_path
)
3008 self
.treeChannels
.set_cursor(new_path
)
3013 def on_iconify(self
):
3015 self
.gPodder
.set_skip_taskbar_hint(True)
3016 if self
.config
.minimize_to_tray
:
3017 self
.tray_icon
.set_visible(True)
3019 self
.gPodder
.set_skip_taskbar_hint(False)
3021 def on_uniconify(self
):
3023 self
.gPodder
.set_skip_taskbar_hint(False)
3024 if self
.config
.minimize_to_tray
:
3025 self
.tray_icon
.set_visible(False)
3027 self
.gPodder
.set_skip_taskbar_hint(False)
3029 def uniconify_main_window(self
):
3030 if self
.is_iconified():
3031 self
.gPodder
.present()
3033 def iconify_main_window(self
):
3034 if not self
.is_iconified():
3035 self
.gPodder
.iconify()
3037 def update_podcasts_tab(self
):
3038 if len(self
.channels
):
3039 if gpodder
.ui
.fremantle
:
3040 self
.button_podcasts
.set_value(_('%d subscriptions') % len(self
.channels
))
3042 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3044 if gpodder
.ui
.fremantle
:
3045 self
.button_podcasts
.set_value(_('No subscriptions'))
3047 self
.label2
.set_text(_('Podcasts'))
3049 @dbus.service
.method(gpodder
.dbus_interface
)
3050 def show_gui_window(self
):
3051 self
.gPodder
.present()
3053 @dbus.service
.method(gpodder
.dbus_interface
)
3054 def subscribe_to_url(self
, url
):
3055 gPodderAddPodcast(self
.gPodder
,
3056 add_urls_callback
=self
.add_podcast_list
,
3059 @dbus.service
.method(gpodder
.dbus_interface
)
3060 def mark_episode_played(self
, filename
):
3061 if filename
is None:
3064 for channel
in self
.channels
:
3065 for episode
in channel
.get_all_episodes():
3066 fn
= episode
.local_filename(create
=False, check_only
=True)
3068 episode
.mark(is_played
=True)
3070 self
.update_episode_list_icons([episode
.url
])
3071 self
.update_podcast_list_model([episode
.channel
.url
])
3077 def main(options
=None):
3078 gobject
.threads_init()
3079 gobject
.set_application_name('gPodder')
3081 if gpodder
.ui
.maemo
:
3082 # Try to enable the custom icon theme for gPodder on Maemo
3083 settings
= gtk
.settings_get_default()
3084 settings
.set_string_property('gtk-icon-theme-name', \
3085 'gpodder', __file__
)
3087 gtk
.window_set_default_icon_name('gpodder')
3088 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3091 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3092 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3093 except dbus
.exceptions
.DBusException
, dbe
:
3094 log('Warning: Cannot get "on the bus".', traceback
=True)
3095 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3096 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3097 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3098 dlg
.set_title('gPodder')
3103 util
.make_directory(gpodder
.home
)
3104 config
= UIConfig(gpodder
.config_file
)
3106 if gpodder
.ui
.diablo
:
3107 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3108 # folder exists there (allow moving "gpodder" between SD cards or USB)
3109 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3110 if not os
.path
.exists(config
.download_dir
):
3111 log('Downloads might have been moved. Trying to locate them...')
3112 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3113 dir = os
.path
.join(basedir
, 'gpodder')
3114 if os
.path
.exists(dir):
3115 log('Downloads found in: %s', dir)
3116 config
.download_dir
= dir
3119 log('Downloads NOT FOUND in %s', dir)
3121 if config
.enable_fingerscroll
:
3122 BuilderWidget
.use_fingerscroll
= True
3123 elif gpodder
.ui
.fremantle
:
3124 # FIXME: Move download_dir from ~/gPodder-Podcasts to default setting
3127 gp
= gPodder(bus_name
, config
)
3130 if options
.subscribe
:
3131 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)