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
101 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
103 except Exception, exc
:
104 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
105 log('Warning: This probably means your PyGTK installation is too old!')
106 have_trayicon
= False
107 from gpodder
.gtkui
.interface
.dependencymanager
import gPodderDependencyManager
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 FremantleAutoRotation
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
, \
163 self
.itemPreferences
):
164 button
= gtk
.Button()
165 action
.connect_proxy(button
)
166 appmenu
.append(button
)
168 self
.main_window
.set_app_menu(appmenu
)
169 self
._fremantle
_update
_banner
= None
171 # Activate application-wide automatic portrait orientation
172 FremantleAutoRotation()
174 self
.bluetooth_available
= False
177 # FIXME: Implement e-mail sending of list in win32
178 self
.item_email_subscriptions
.set_sensitive(False)
179 self
.bluetooth_available
= util
.bluetooth_available()
180 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
182 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
183 if not gpodder
.ui
.fremantle
:
184 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
185 self
.main_window
.show()
187 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
189 self
.config
.add_observer(self
.on_config_changed
)
191 self
.tray_icon
= None
192 self
.episode_shownotes_window
= None
194 if gpodder
.ui
.desktop
:
195 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
196 self
.main_window
, self
.show_confirmation
, \
197 self
.update_episode_list_icons
, \
198 self
.update_podcast_list_model
, self
.toolPreferences
, \
199 gPodderEpisodeSelector
)
203 self
.download_status_model
= DownloadStatusModel()
204 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
206 if gpodder
.ui
.desktop
:
207 self
.show_hide_tray_icon()
208 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
209 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
211 if not gpodder
.ui
.fremantle
:
212 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
213 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
214 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
215 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
217 # When the amount of maximum downloads changes, notify the queue manager
218 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_and_retire_threads()
219 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
221 self
.default_title
= 'gPodder'
222 if gpodder
.__version
__.rfind('git') != -1:
223 self
.set_title('gPodder %s' % gpodder
.__version
__)
225 title
= self
.gPodder
.get_title()
226 if title
is not None:
227 self
.set_title(title
)
229 self
.set_title(_('gPodder'))
231 self
.cover_downloader
= CoverDownloader()
233 # Generate list models for podcasts and their episodes
234 self
.podcast_list_model
= PodcastListModel(self
.config
.podcast_list_icon_size
, self
.cover_downloader
)
236 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
237 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
239 if gpodder
.ui
.fremantle
:
240 self
.button_subscribe
.set_name('HildonButton-thumb')
241 self
.button_podcasts
.set_name('HildonButton-thumb')
242 self
.button_downloads
.set_name('HildonButton-thumb')
244 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
245 while gtk
.events_pending():
246 gtk
.main_iteration(False)
248 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
249 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
250 show_episode_shownotes
=self
.show_episode_shownotes
, \
251 update_podcast_list_model
=self
.update_podcast_list_model
, \
252 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
253 item_view_episodes_all
=self
.item_view_episodes_all
, \
254 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
255 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
256 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
)
258 def on_podcast_selected(channel
):
259 self
.active_channel
= channel
260 self
.update_episode_list_model()
261 self
.episodes_window
.channel
= self
.active_channel
262 self
.episodes_window
.show()
264 self
.podcasts_window
= gPodderPodcasts(self
.main_window
, \
265 show_podcast_episodes
=on_podcast_selected
, \
266 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
267 on_itemAddChannel_activate
=self
.on_itemAddChannel_activate
, \
268 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
269 item_view_podcasts_all
=self
.item_view_podcasts_all
, \
270 item_view_podcasts_downloaded
=self
.item_view_podcasts_downloaded
, \
271 item_view_podcasts_unplayed
=self
.item_view_podcasts_unplayed
)
273 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
274 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
275 on_btnCleanUpDownloads_clicked
=self
.on_btnCleanUpDownloads_clicked
)
276 self
.treeChannels
= self
.podcasts_window
.treeview
277 self
.treeAvailable
= self
.episodes_window
.treeview
278 self
.treeDownloads
= self
.downloads_window
.treeview
280 # Init the treeviews that we use
281 self
.init_podcast_list_treeview()
282 self
.init_episode_list_treeview()
283 self
.init_download_list_treeview()
285 if self
.config
.podcast_list_hide_boring
:
286 self
.item_view_hide_boring_podcasts
.set_active(True)
288 self
.currently_updating
= False
291 self
.context_menu_mouse_button
= 1
293 self
.context_menu_mouse_button
= 3
295 if self
.config
.start_iconified
:
296 self
.iconify_main_window()
298 self
.download_tasks_seen
= set()
299 self
.download_list_update_enabled
= False
300 self
.last_download_count
= 0
302 # Subscribed channels
303 self
.active_channel
= None
304 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
305 self
.channel_list_changed
= True
306 self
.update_podcasts_tab()
308 # load list of user applications for audio playback
309 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
311 time
.sleep(3) # give other parts of gpodder a chance to start up
312 self
.user_apps_reader
.read()
313 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
314 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
315 threading
.Thread(target
=read_apps
).start()
317 # Set the "Device" menu item for the first time
318 if gpodder
.ui
.desktop
:
319 self
.update_item_device()
321 # Now, update the feed cache, when everything's in place
322 if not gpodder
.ui
.fremantle
:
323 self
.btnUpdateFeeds
.show()
324 self
.updating_feed_cache
= False
325 self
.feed_cache_update_cancelled
= False
326 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
328 # Look for partial file downloads
329 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
332 self
.message_area
= None
334 resumable_episodes
= []
335 if len(partial_files
) > 0:
336 for f
in partial_files
:
337 correct_name
= f
[:-len('.partial')] # strip ".partial"
338 log('Searching episode for file: %s', correct_name
, sender
=self
)
339 found_episode
= False
340 for c
in self
.channels
:
341 for e
in c
.get_all_episodes():
342 if e
.local_filename(create
=False, check_only
=True) == correct_name
:
343 log('Found episode: %s', e
.title
, sender
=self
)
344 resumable_episodes
.append(e
)
350 if not found_episode
:
351 log('Partial file without episode: %s', f
, sender
=self
)
354 if len(resumable_episodes
):
355 self
.download_episode_list_paused(resumable_episodes
)
356 if not gpodder
.ui
.fremantle
:
357 self
.message_area
= SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
358 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
359 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
360 self
.message_area
.show_all()
361 self
.wNotebook
.set_current_page(1)
363 self
.clean_up_downloads(delete_partial
=False)
365 self
.clean_up_downloads(delete_partial
=True)
367 # Start the auto-update procedure
368 self
.auto_update_procedure(first_run
=True)
370 # Delete old episodes if the user wishes to
371 if self
.config
.auto_remove_old_episodes
:
372 old_episodes
= self
.get_old_episodes()
373 if len(old_episodes
) > 0:
374 self
.delete_episode_list(old_episodes
, confirm
=False)
375 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
377 if gpodder
.ui
.fremantle
:
378 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
379 self
.button_subscribe
.set_sensitive(True)
380 self
.button_podcasts
.set_sensitive(True)
381 self
.button_downloads
.set_sensitive(True)
383 # First-time users should be asked if they want to see the OPML
384 if not self
.channels
and not gpodder
.ui
.fremantle
:
385 util
.idle_add(self
.on_itemUpdate_activate
)
387 def on_button_subscribe_clicked(self
, button
):
388 self
.on_itemImportChannels_activate(button
)
390 def on_button_podcasts_clicked(self
, widget
):
392 self
.podcasts_window
.show()
394 gPodderWelcome(self
.gPodder
, \
395 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
, \
396 setup_my_gpodder_callback
=self
.on_download_from_mygpo
)
398 def on_button_downloads_clicked(self
, widget
):
399 self
.downloads_window
.show()
401 def on_window_orientation_changed(self
, orientation
):
402 old_container
= self
.main_window
.get_child()
403 if orientation
== Orientation
.PORTRAIT
:
404 container
= gtk
.VButtonBox()
406 container
= gtk
.HButtonBox()
407 container
.set_layout(old_container
.get_layout())
408 for child
in old_container
.get_children():
409 if orientation
== Orientation
.LANDSCAPE
:
410 child
.set_alignment(0.5, 0.5, 0., 0.)
411 child
.set_property('width-request', 200)
413 child
.set_alignment(0.5, 0.5, .9, 0.)
414 child
.set_property('width-request', 350)
415 child
.reparent(container
)
417 self
.buttonbox
= container
418 self
.main_window
.remove(old_container
)
419 self
.main_window
.add(container
)
421 def on_treeview_podcasts_selection_changed(self
, selection
):
422 model
, iter = selection
.get_selected()
424 self
.active_channel
= None
425 self
.episode_list_model
.clear()
427 def on_treeview_button_pressed(self
, treeview
, event
):
428 TreeViewHelper
.save_button_press_event(treeview
, event
)
430 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
431 TreeViewHelper
.ROLE_PODCASTS
:
432 return self
.currently_updating
434 return event
.button
== self
.context_menu_mouse_button
and \
437 def on_treeview_podcasts_button_released(self
, treeview
, event
):
439 return self
.treeview_channels_handle_gestures(treeview
, event
)
441 return self
.treeview_channels_show_context_menu(treeview
, event
)
443 def on_treeview_episodes_button_released(self
, treeview
, event
):
445 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
446 return self
.treeview_available_handle_gestures(treeview
, event
)
448 return self
.treeview_available_show_context_menu(treeview
, event
)
450 def on_treeview_downloads_button_released(self
, treeview
, event
):
451 return self
.treeview_downloads_show_context_menu(treeview
, event
)
453 def init_podcast_list_treeview(self
):
454 # Set up podcast channel tree view widget
455 self
.treeChannels
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(PodcastListModel
))
457 if gpodder
.ui
.fremantle
:
458 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
459 self
.item_view_podcasts_downloaded
.set_active(True)
460 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
461 self
.item_view_podcasts_unplayed
.set_active(True)
463 self
.item_view_podcasts_all
.set_active(True)
464 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
466 iconcolumn
= gtk
.TreeViewColumn('')
467 iconcell
= gtk
.CellRendererPixbuf()
468 iconcolumn
.pack_start(iconcell
, False)
469 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
470 self
.treeChannels
.append_column(iconcolumn
)
472 namecolumn
= gtk
.TreeViewColumn('')
473 namecell
= gtk
.CellRendererText()
474 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
475 namecolumn
.pack_start(namecell
, True)
476 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
478 iconcell
= gtk
.CellRendererPixbuf()
479 iconcell
.set_property('xalign', 1.0)
480 namecolumn
.pack_start(iconcell
, False)
481 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
482 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
483 self
.treeChannels
.append_column(namecolumn
)
485 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
487 # When no podcast is selected, clear the episode list model
488 selection
= self
.treeChannels
.get_selection()
489 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
491 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
493 def init_episode_list_treeview(self
):
494 self
.episode_list_model
= EpisodeListModel()
496 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
497 self
.item_view_episodes_undeleted
.set_active(True)
498 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
499 self
.item_view_episodes_downloaded
.set_active(True)
500 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
501 self
.item_view_episodes_unplayed
.set_active(True)
503 self
.item_view_episodes_all
.set_active(True)
505 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
507 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
509 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
511 iconcell
= gtk
.CellRendererPixbuf()
513 iconcell
.set_fixed_size(50, 50)
514 status_column_label
= ''
516 status_column_label
= _('Status')
517 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
519 namecell
= gtk
.CellRendererText()
520 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
521 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
522 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
523 namecolumn
.set_resizable(True)
524 namecolumn
.set_expand(True)
526 sizecell
= gtk
.CellRendererText()
527 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
529 releasecell
= gtk
.CellRendererText()
530 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
532 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
533 itemcolumn
.set_reorderable(True)
534 self
.treeAvailable
.append_column(itemcolumn
)
537 sizecolumn
.set_visible(False)
538 releasecolumn
.set_visible(False)
540 self
.treeAvailable
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(EpisodeListModel
))
542 selection
= self
.treeAvailable
.get_selection()
543 if gpodder
.ui
.diablo
:
544 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
545 selection
.set_mode(gtk
.SELECTION_SINGLE
)
547 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
548 elif gpodder
.ui
.fremantle
:
549 selection
.set_mode(gtk
.SELECTION_SINGLE
)
551 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
552 # Update the sensitivity of the toolbar buttons on the Desktop
553 selection
.connect('changed', lambda s
: self
.play_or_download())
555 if gpodder
.ui
.diablo
:
556 # Set up the tap-and-hold context menu for podcasts
558 menu
.append(self
.itemUpdateChannel
.create_menu_item())
559 menu
.append(self
.itemEditChannel
.create_menu_item())
560 menu
.append(gtk
.SeparatorMenuItem())
561 menu
.append(self
.itemRemoveChannel
.create_menu_item())
562 menu
.append(gtk
.SeparatorMenuItem())
563 item
= gtk
.ImageMenuItem(_('Close this menu'))
564 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
568 menu
= self
.set_finger_friendly(menu
)
569 self
.treeChannels
.tap_and_hold_setup(menu
)
572 def init_download_list_treeview(self
):
573 # enable multiple selection support
574 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
575 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
577 # columns and renderers for "download progress" tab
578 # First column: [ICON] Episodename
579 column
= gtk
.TreeViewColumn(_('Episode'))
581 cell
= gtk
.CellRendererPixbuf()
583 cell
.set_fixed_size(50, 50)
584 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
585 column
.pack_start(cell
, expand
=False)
586 column
.add_attribute(cell
, 'stock-id', \
587 DownloadStatusModel
.C_ICON_NAME
)
589 cell
= gtk
.CellRendererText()
590 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
591 column
.pack_start(cell
, expand
=True)
592 column
.add_attribute(cell
, 'text', DownloadStatusModel
.C_NAME
)
594 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
595 column
.set_resizable(True)
596 column
.set_expand(True)
597 self
.treeDownloads
.append_column(column
)
599 # Second column: Progress
600 column
= gtk
.TreeViewColumn(_('Progress'), gtk
.CellRendererProgress(),
601 value
=DownloadStatusModel
.C_PROGRESS
, \
602 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
603 self
.treeDownloads
.append_column(column
)
606 if gpodder
.ui
.desktop
:
607 column
= gtk
.TreeViewColumn(_('Size'), gtk
.CellRendererText(),
608 text
=DownloadStatusModel
.C_SIZE_TEXT
)
609 self
.treeDownloads
.append_column(column
)
611 # Fourth column: Speed
612 column
= gtk
.TreeViewColumn(_('Speed'), gtk
.CellRendererText(),
613 text
=DownloadStatusModel
.C_SPEED_TEXT
)
614 self
.treeDownloads
.append_column(column
)
616 if not gpodder
.ui
.fremantle
:
617 # Fifth column: Status
618 column
= gtk
.TreeViewColumn(_('Status'), gtk
.CellRendererText(),
619 text
=DownloadStatusModel
.C_STATUS_TEXT
)
620 self
.treeDownloads
.append_column(column
)
622 self
.treeDownloads
.set_model(self
.download_status_model
)
623 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
625 def on_treeview_expose_event(self
, treeview
, event
):
626 if event
.window
== treeview
.get_bin_window():
627 model
= treeview
.get_model()
628 if (model
is not None and model
.get_iter_first() is not None):
631 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
632 ctx
= event
.window
.cairo_create()
633 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
634 event
.area
.width
, event
.area
.height
)
637 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
639 if role
== TreeViewHelper
.ROLE_EPISODES
:
640 if self
.currently_updating
:
641 text
= _('Loading episodes') + '...'
642 elif self
.config
.episode_list_view_mode
!= \
643 EpisodeListModel
.VIEW_ALL
:
644 text
= _('No episodes in current view')
646 text
= _('No episodes available')
647 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
648 if self
.config
.episode_list_view_mode
!= \
649 EpisodeListModel
.VIEW_ALL
and \
650 self
.config
.podcast_list_hide_boring
and \
651 len(self
.channels
) > 0:
652 text
= _('No podcasts in this view')
654 text
= _('No subscriptions')
655 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
656 text
= _('No active downloads')
658 raise Exception('on_treeview_expose_event: unknown role')
660 if gpodder
.ui
.fremantle
:
661 from gpodder
.gtkui
.frmntl
import style
662 font_desc
= style
.get_font_desc('LargeSystemFont')
666 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
)
670 def enable_download_list_update(self
):
671 if not self
.download_list_update_enabled
:
672 gobject
.timeout_add(1500, self
.update_downloads_list
)
673 self
.download_list_update_enabled
= True
675 def on_btnCleanUpDownloads_clicked(self
, button
):
676 model
= self
.download_status_model
678 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
679 changed_episode_urls
= []
680 for row_reference
, task
in all_tasks
:
681 if task
.status
in (task
.DONE
, task
.CANCELLED
, task
.FAILED
):
682 model
.remove(model
.get_iter(row_reference
.get_path()))
684 # We don't "see" this task anymore - remove it;
685 # this is needed, so update_episode_list_icons()
686 # below gets the correct list of "seen" tasks
687 self
.download_tasks_seen
.remove(task
)
688 except KeyError, key_error
:
689 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
690 changed_episode_urls
.append(task
.url
)
691 # Tell the task that it has been removed (so it can clean up)
692 task
.removed_from_list()
694 # Tell the podcasts tab to update icons for our removed podcasts
695 self
.update_episode_list_icons(changed_episode_urls
)
697 # Tell the shownotes window that we have removed the episode
698 if self
.episode_shownotes_window
is not None and \
699 self
.episode_shownotes_window
.episode
is not None and \
700 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
701 self
.episode_shownotes_window
._download
_status
_changed
(None)
703 # Update the tab title and downloads list
704 self
.update_downloads_list()
706 def on_tool_downloads_toggled(self
, toolbutton
):
707 if toolbutton
.get_active():
708 self
.wNotebook
.set_current_page(1)
710 self
.wNotebook
.set_current_page(0)
712 def update_downloads_list(self
):
714 model
= self
.download_status_model
716 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
717 total_speed
, total_size
, done_size
= 0, 0, 0
719 # Keep a list of all download tasks that we've seen
720 download_tasks_seen
= set()
722 # Remember the DownloadTask object for the episode that
723 # has been opened in the episode shownotes dialog (if any)
724 if self
.episode_shownotes_window
is not None:
725 shownotes_episode
= self
.episode_shownotes_window
.episode
726 shownotes_task
= None
728 shownotes_episode
= None
729 shownotes_task
= None
731 # Do not go through the list of the model is not (yet) available
736 self
.download_status_model
.request_update(row
.iter)
738 task
= row
[self
.download_status_model
.C_TASK
]
739 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
742 done_size
+= size
*progress
744 if shownotes_episode
is not None and \
745 shownotes_episode
.url
== task
.episode
.url
:
746 shownotes_task
= task
748 download_tasks_seen
.add(task
)
750 if status
== download
.DownloadTask
.DOWNLOADING
:
753 elif status
== download
.DownloadTask
.FAILED
:
755 elif status
== download
.DownloadTask
.DONE
:
757 elif status
== download
.DownloadTask
.QUEUED
:
759 elif status
== download
.DownloadTask
.PAUSED
:
764 # Remember which tasks we have seen after this run
765 self
.download_tasks_seen
= download_tasks_seen
767 if gpodder
.ui
.desktop
:
768 text
= [_('Downloads')]
769 if downloading
+ failed
+ finished
+ queued
> 0:
772 s
.append(_('%d active') % downloading
)
774 s
.append(_('%d failed') % failed
)
776 s
.append(_('%d done') % finished
)
778 s
.append(_('%d queued') % queued
)
779 text
.append(' (' + ', '.join(s
)+')')
780 self
.labelDownloads
.set_text(''.join(text
))
781 elif gpodder
.ui
.diablo
:
782 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
784 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
786 self
.tool_downloads
.set_label(_('Downloads'))
787 elif gpodder
.ui
.fremantle
:
788 if downloading
+ queued
> 0:
789 self
.button_downloads
.set_value(_('%d active') % (downloading
+queued
))
791 self
.button_downloads
.set_value(_('%d failed') % failed
)
793 self
.button_downloads
.set_value(_('%d paused') % paused
)
795 self
.button_downloads
.set_value(_('None active'))
797 title
= [self
.default_title
]
799 # We have to update all episodes/channels for which the status has
800 # changed. Accessing task.status_changed has the side effect of
801 # re-setting the changed flag, so we need to get the "changed" list
802 # of tuples first and split it into two lists afterwards
803 changed
= [(task
.url
, task
.podcast_url
) for task
in \
804 self
.download_tasks_seen
if task
.status_changed
]
805 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
806 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
808 count
= downloading
+ queued
811 title
.append( _('downloading one file'))
813 title
.append( _('downloading %d files') % count
)
816 percentage
= 100.0*done_size
/total_size
819 total_speed
= util
.format_filesize(total_speed
)
820 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
821 if self
.tray_icon
is not None:
822 # Update the tray icon status and progress bar
823 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
824 self
.tray_icon
.draw_progress_bar(percentage
/100.)
825 elif self
.last_download_count
> 0:
826 if self
.tray_icon
is not None:
827 # Update the tray icon status
828 self
.tray_icon
.set_status()
829 self
.tray_icon
.downloads_finished(self
.download_tasks_seen
)
830 if gpodder
.ui
.diablo
:
831 hildon
.hildon_banner_show_information(self
.gPodder
, None, 'gPodder: %s' % _('All downloads finished'))
832 log('All downloads have finished.', sender
=self
)
833 if self
.config
.cmd_all_downloads_complete
:
834 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
835 self
.last_download_count
= count
837 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 treeview_downloads_show_context_menu(self
, treeview
, event
):
991 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
993 if not hasattr(treeview
, 'is_rubber_banding_active'):
996 return not treeview
.is_rubber_banding_active()
998 if event
.button
== self
.context_menu_mouse_button
:
999 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
1001 def make_menu_item(label
, stock_id
, tasks
, status
):
1002 # This creates a menu item for selection-wide actions
1003 def for_each_task_set_status(tasks
, status
):
1004 changed_episode_urls
= []
1005 for row_reference
, task
in tasks
:
1006 if status
is not None:
1007 if status
== download
.DownloadTask
.QUEUED
:
1008 # Only queue task when its paused/failed/cancelled
1009 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
):
1010 self
.download_queue_manager
.add_task(task
)
1011 self
.enable_download_list_update()
1012 elif status
== download
.DownloadTask
.CANCELLED
:
1013 # Cancelling a download allowed when downloading/queued
1014 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1015 task
.status
= status
1016 # Cancelling paused downloads requires a call to .run()
1017 elif task
.status
== task
.PAUSED
:
1018 task
.status
= status
1019 # Call run, so the partial file gets deleted
1021 elif status
== download
.DownloadTask
.PAUSED
:
1022 # Pausing a download only when queued/downloading
1023 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1024 task
.status
= status
1026 # We (hopefully) can simply set the task status here
1027 task
.status
= status
1029 # Remove the selected task - cancel downloading/queued tasks
1030 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1031 task
.status
= task
.CANCELLED
1032 model
.remove(model
.get_iter(row_reference
.get_path()))
1033 # Remember the URL, so we can tell the UI to update
1035 # We don't "see" this task anymore - remove it;
1036 # this is needed, so update_episode_list_icons()
1037 # below gets the correct list of "seen" tasks
1038 self
.download_tasks_seen
.remove(task
)
1039 except KeyError, key_error
:
1040 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1041 changed_episode_urls
.append(task
.url
)
1042 # Tell the task that it has been removed (so it can clean up)
1043 task
.removed_from_list()
1044 # Tell the podcasts tab to update icons for our removed podcasts
1045 self
.update_episode_list_icons(changed_episode_urls
)
1046 # Update the tab title and downloads list
1047 self
.update_downloads_list()
1049 item
= gtk
.ImageMenuItem(label
)
1050 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1051 item
.connect('activate', lambda item
: for_each_task_set_status(tasks
, status
))
1053 # Determine if we should disable this menu item
1054 for row_reference
, task
in tasks
:
1055 if status
== download
.DownloadTask
.QUEUED
:
1056 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1057 download
.DownloadTask
.FAILED
, \
1058 download
.DownloadTask
.CANCELLED
):
1059 item
.set_sensitive(False)
1061 elif status
== download
.DownloadTask
.CANCELLED
:
1062 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1063 download
.DownloadTask
.QUEUED
, \
1064 download
.DownloadTask
.DOWNLOADING
):
1065 item
.set_sensitive(False)
1067 elif status
== download
.DownloadTask
.PAUSED
:
1068 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1069 download
.DownloadTask
.DOWNLOADING
):
1070 item
.set_sensitive(False)
1072 elif status
is None:
1073 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1074 download
.DownloadTask
.FAILED
, \
1075 download
.DownloadTask
.DONE
):
1076 item
.set_sensitive(False)
1079 return self
.set_finger_friendly(item
)
1083 item
= gtk
.ImageMenuItem(_('Episode details'))
1084 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1085 if len(selected_tasks
) == 1:
1086 row_reference
, task
= selected_tasks
[0]
1087 episode
= task
.episode
1088 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1090 item
.set_sensitive(False)
1091 menu
.append(self
.set_finger_friendly(item
))
1092 menu
.append(gtk
.SeparatorMenuItem())
1093 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
))
1094 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
))
1095 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
))
1096 menu
.append(gtk
.SeparatorMenuItem())
1097 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None))
1099 if gpodder
.ui
.maemo
:
1100 # Because we open the popup on left-click for Maemo,
1101 # we also include a non-action to close the menu
1102 menu
.append(gtk
.SeparatorMenuItem())
1103 item
= gtk
.ImageMenuItem(_('Close this menu'))
1104 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1106 menu
.append(self
.set_finger_friendly(item
))
1109 menu
.popup(None, None, None, event
.button
, event
.time
)
1112 def treeview_channels_show_context_menu(self
, treeview
, event
):
1113 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1117 if event
.button
== 3:
1122 item
= gtk
.ImageMenuItem( _('Open download folder'))
1123 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1124 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1127 item
= gtk
.ImageMenuItem( _('Update Feed'))
1128 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1129 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1130 item
.set_sensitive( not self
.updating_feed_cache
)
1133 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1134 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1135 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1138 if self
.active_channel
.link
:
1139 item
= gtk
.ImageMenuItem(_('Visit website'))
1140 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1141 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1144 if self
.active_channel
.channel_is_locked
:
1145 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1146 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1147 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1148 menu
.append(self
.set_finger_friendly(item
))
1150 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1151 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1152 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1153 menu
.append(self
.set_finger_friendly(item
))
1156 menu
.append( gtk
.SeparatorMenuItem())
1158 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1159 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1162 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1163 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1167 # Disable tooltips while we are showing the menu, so
1168 # the tooltip will not appear over the menu
1169 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1170 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1171 menu
.popup( None, None, None, event
.button
, event
.time
)
1175 def on_itemClose_activate(self
, widget
):
1176 if self
.tray_icon
is not None:
1177 self
.iconify_main_window()
1179 self
.on_gPodder_delete_event(widget
)
1181 def cover_file_removed(self
, channel_url
):
1183 The Cover Downloader calls this when a previously-
1184 available cover has been removed from the disk. We
1185 have to update our model to reflect this change.
1187 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1189 def cover_download_finished(self
, channel_url
, pixbuf
):
1191 The Cover Downloader calls this when it has finished
1192 downloading (or registering, if already downloaded)
1193 a new channel cover, which is ready for displaying.
1195 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1197 def save_episode_as_file(self
, episode
):
1198 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1199 if episode
.was_downloaded(and_exists
=True):
1200 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1201 copy_from
= episode
.local_filename(create
=False)
1202 assert copy_from
is not None
1203 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1204 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1205 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1207 def copy_episodes_bluetooth(self
, episodes
):
1208 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1210 def convert_and_send_thread(episode
):
1211 for episode
in episodes
:
1212 filename
= episode
.local_filename(create
=False)
1213 assert filename
is not None
1214 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1215 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1216 (base
, ext
) = os
.path
.splitext(filename
)
1217 if not destfile
.endswith(ext
):
1221 shutil
.copyfile(filename
, destfile
)
1222 util
.bluetooth_send_file(destfile
)
1224 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1225 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1227 util
.delete_file(destfile
)
1229 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1231 def get_device_name(self
):
1232 if self
.config
.device_type
== 'ipod':
1234 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1235 return _('MP3 player')
1237 return '(unknown device)'
1239 def _treeview_button_released(self
, treeview
, event
):
1240 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1241 dy
= int(abs(event
.y
-ypos
))
1242 dx
= int(event
.x
-xpos
)
1244 selection
= treeview
.get_selection()
1245 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1246 if path
is None or dy
> 30:
1247 return (False, dx
, dy
)
1249 path
, column
, x
, y
= path
1250 selection
.select_path(path
)
1251 treeview
.set_cursor(path
)
1252 treeview
.grab_focus()
1254 return (True, dx
, dy
)
1256 def treeview_channels_handle_gestures(self
, treeview
, event
):
1257 if self
.currently_updating
:
1260 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1263 if self
.config
.maemo_enable_gestures
:
1265 self
.on_itemUpdateChannel_activate()
1267 self
.on_itemEditChannel_activate(treeview
)
1271 def treeview_available_handle_gestures(self
, treeview
, event
):
1272 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1275 if self
.config
.maemo_enable_gestures
:
1277 self
.on_playback_selected_episodes(None)
1280 self
.on_shownotes_selected_episodes(None)
1283 # Pass the event to the context menu handler for treeAvailable
1284 self
.treeview_available_show_context_menu(treeview
, event
)
1288 def treeview_available_show_context_menu(self
, treeview
, event
):
1289 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1291 if not hasattr(treeview
, 'is_rubber_banding_active'):
1294 return not treeview
.is_rubber_banding_active()
1296 if event
.button
== self
.context_menu_mouse_button
:
1297 episodes
= self
.get_selected_episodes()
1298 any_locked
= any(e
.is_locked
for e
in episodes
)
1299 any_played
= any(e
.is_played
for e
in episodes
)
1300 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1304 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1306 if open_instead_of_play
:
1307 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1309 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1311 item
.set_sensitive(can_play
)
1312 item
.connect('activate', self
.on_playback_selected_episodes
)
1313 menu
.append(self
.set_finger_friendly(item
))
1316 item
= gtk
.ImageMenuItem(_('Download'))
1317 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1318 item
.set_sensitive(can_download
)
1319 item
.connect('activate', self
.on_download_selected_episodes
)
1320 menu
.append(self
.set_finger_friendly(item
))
1322 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1323 item
.connect('activate', self
.on_item_cancel_download_activate
)
1324 menu
.append(self
.set_finger_friendly(item
))
1326 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1327 item
.set_sensitive(can_delete
)
1328 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1329 menu
.append(self
.set_finger_friendly(item
))
1332 item
= gtk
.ImageMenuItem(_('Do not download'))
1333 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1334 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1335 menu
.append(self
.set_finger_friendly(item
))
1337 item
= gtk
.ImageMenuItem(_('Mark as new'))
1338 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
1339 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1340 menu
.append(self
.set_finger_friendly(item
))
1344 # Ok, this probably makes sense to only display for downloaded files
1345 if can_play
and not can_download
:
1346 menu
.append( gtk
.SeparatorMenuItem())
1347 item
= gtk
.ImageMenuItem(_('Save to disk'))
1348 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
1349 item
.connect('activate', lambda w
: [self
.save_episode_as_file(e
) for e
in episodes
])
1350 menu
.append(self
.set_finger_friendly(item
))
1351 if self
.bluetooth_available
:
1352 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
1353 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1354 item
.connect('activate', lambda w
: self
.copy_episodes_bluetooth(episodes
))
1355 menu
.append(self
.set_finger_friendly(item
))
1357 item
= gtk
.ImageMenuItem(_('Transfer to %s') % self
.get_device_name())
1358 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1359 item
.connect('activate', lambda w
: self
.on_sync_to_ipod_activate(w
, episodes
))
1360 menu
.append(self
.set_finger_friendly(item
))
1363 menu
.append( gtk
.SeparatorMenuItem())
1365 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
1366 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
1367 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
1368 menu
.append(self
.set_finger_friendly(item
))
1370 item
= gtk
.ImageMenuItem(_('Mark as played'))
1371 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
1372 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
1373 menu
.append(self
.set_finger_friendly(item
))
1376 item
= gtk
.ImageMenuItem(_('Allow deletion'))
1377 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1378 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, False))
1379 menu
.append(self
.set_finger_friendly(item
))
1381 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
1382 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1383 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, True))
1384 menu
.append(self
.set_finger_friendly(item
))
1386 menu
.append(gtk
.SeparatorMenuItem())
1387 # Single item, add episode information menu item
1388 item
= gtk
.ImageMenuItem(_('Episode details'))
1389 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1390 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1391 menu
.append(self
.set_finger_friendly(item
))
1393 # If we have it, also add episode website link
1394 if episodes
[0].link
and episodes
[0].link
!= episodes
[0].url
:
1395 item
= gtk
.ImageMenuItem(_('Visit website'))
1396 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1397 item
.connect('activate', lambda w
: util
.open_website(episodes
[0].link
))
1398 menu
.append(self
.set_finger_friendly(item
))
1400 if gpodder
.ui
.maemo
:
1401 # Because we open the popup on left-click for Maemo,
1402 # we also include a non-action to close the menu
1403 menu
.append(gtk
.SeparatorMenuItem())
1404 item
= gtk
.ImageMenuItem(_('Close this menu'))
1405 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1406 menu
.append(self
.set_finger_friendly(item
))
1409 # Disable tooltips while we are showing the menu, so
1410 # the tooltip will not appear over the menu
1411 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1412 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1413 menu
.popup( None, None, None, event
.button
, event
.time
)
1417 def set_title(self
, new_title
):
1418 if not gpodder
.ui
.fremantle
:
1419 self
.default_title
= new_title
1420 self
.gPodder
.set_title(new_title
)
1422 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1424 Updates the status icons in the episode list.
1426 If urls is given, it should be a list of URLs
1427 of episodes that should be updated.
1429 If urls is None, set ONE OF selected, all to
1430 True (the former updates just the selected
1431 episodes and the latter updates all episodes).
1433 if urls
is not None:
1434 # We have a list of URLs to walk through
1435 self
.episode_list_model
.update_by_urls(urls
, \
1436 self
.episode_is_downloading
, \
1437 self
.config
.episode_list_descriptions
and \
1439 elif selected
and not all
:
1440 # We should update all selected episodes
1441 selection
= self
.treeAvailable
.get_selection()
1442 model
, paths
= selection
.get_selected_rows()
1443 for path
in reversed(paths
):
1444 iter = model
.get_iter(path
)
1445 self
.episode_list_model
.update_by_filter_iter(iter, \
1446 self
.episode_is_downloading
, \
1447 self
.config
.episode_list_descriptions
and \
1449 elif all
and not selected
:
1450 # We update all (even the filter-hidden) episodes
1451 self
.episode_list_model
.update_all(\
1452 self
.episode_is_downloading
, \
1453 self
.config
.episode_list_descriptions
and \
1456 # Wrong/invalid call - have to specify at least one parameter
1457 raise ValueError('Invalid call to update_episode_list_icons')
1459 def episode_list_status_changed(self
, episodes
):
1460 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1461 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1464 def clean_up_downloads(self
, delete_partial
=False):
1465 # Clean up temporary files left behind by old gPodder versions
1466 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
1469 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
1471 for tempfile
in temporary_files
:
1472 util
.delete_file(tempfile
)
1474 # Clean up empty download folders and abandoned download folders
1475 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
1476 for ddir
in download_dirs
:
1477 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1478 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
1479 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
1480 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
1481 shutil
.rmtree(ddir
, ignore_errors
=True)
1483 def streaming_possible(self
):
1484 return self
.config
.player
and \
1485 self
.config
.player
!= 'default' and \
1488 def playback_episodes_for_real(self
, episodes
):
1489 groups
= collections
.defaultdict(list)
1490 for episode
in episodes
:
1491 file_type
= episode
.file_type()
1492 if file_type
== 'video' and self
.config
.videoplayer
and \
1493 self
.config
.videoplayer
!= 'default':
1494 player
= self
.config
.videoplayer
1495 if gpodder
.ui
.diablo
:
1496 # Use the wrapper script if it's installed to crop 3GP YouTube
1497 # videos to fit the screen (looks much nicer than w/ black border)
1498 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
1499 player
= 'gpodder-mplayer'
1500 elif file_type
== 'audio' and self
.config
.player
and \
1501 self
.config
.player
!= 'default':
1502 player
= self
.config
.player
1506 if file_type
not in ('audio', 'video') or \
1507 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
1508 (file_type
== 'video' and not self
.config
.video_played_dbus
):
1509 # Mark episode as played in the database
1510 episode
.mark(is_played
=True)
1512 filename
= episode
.local_filename(create
=False)
1513 if filename
is None or not os
.path
.exists(filename
):
1514 filename
= episode
.url
1515 groups
[player
].append(filename
)
1517 # Open episodes with system default player
1518 if 'default' in groups
:
1519 for filename
in groups
['default']:
1520 log('Opening with system default: %s', filename
, sender
=self
)
1521 util
.gui_open(filename
)
1522 del groups
['default']
1524 # For each type now, go and create play commands
1525 for group
in groups
:
1526 for command
in util
.format_desktop_command(group
, groups
[group
]):
1527 log('Executing: %s', repr(command
), sender
=self
)
1528 subprocess
.Popen(command
)
1530 def playback_episodes(self
, episodes
):
1531 if gpodder
.ui
.maemo
:
1532 if len(episodes
) == 1:
1533 text
= _('Opening %s') % episodes
[0].title
1535 text
= _('Opening %d episodes') % len(episodes
)
1537 if gpodder
.ui
.diablo
:
1538 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, text
)
1539 elif gpodder
.ui
.fremantle
:
1540 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
1542 def destroy_banner_later(banner
):
1545 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
1547 episodes
= [e
for e
in episodes
if \
1548 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()]
1551 self
.playback_episodes_for_real(episodes
)
1552 except Exception, e
:
1553 log('Error in playback!', sender
=self
, traceback
=True)
1554 self
.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget
=self
.toolPreferences
)
1556 channel_urls
= set()
1557 episode_urls
= set()
1558 for episode
in episodes
:
1559 channel_urls
.add(episode
.channel
.url
)
1560 episode_urls
.add(episode
.url
)
1561 self
.update_episode_list_icons(episode_urls
)
1562 self
.update_podcast_list_model(channel_urls
)
1564 def play_or_download(self
):
1565 if not gpodder
.ui
.fremantle
:
1566 if self
.wNotebook
.get_current_page() > 0:
1567 if gpodder
.ui
.desktop
:
1568 self
.toolCancel
.set_sensitive(True)
1571 if self
.currently_updating
:
1572 return (False, False, False, False, False, False)
1574 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1575 ( is_played
, is_locked
) = (False,)*2
1577 open_instead_of_play
= False
1579 selection
= self
.treeAvailable
.get_selection()
1580 if selection
.count_selected_rows() > 0:
1581 (model
, paths
) = selection
.get_selected_rows()
1584 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
1586 if episode
.file_type() not in ('audio', 'video'):
1587 open_instead_of_play
= True
1589 if episode
.was_downloaded():
1590 can_play
= episode
.was_downloaded(and_exists
=True)
1592 is_played
= episode
.is_played
1593 is_locked
= episode
.is_locked
1597 if self
.episode_is_downloading(episode
):
1602 can_download
= can_download
and not can_cancel
1603 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
1604 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
1606 if gpodder
.ui
.desktop
:
1607 if open_instead_of_play
:
1608 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
1610 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
1611 self
.toolPlay
.set_sensitive( can_play
)
1612 self
.toolDownload
.set_sensitive( can_download
)
1613 self
.toolTransfer
.set_sensitive( can_transfer
)
1614 self
.toolCancel
.set_sensitive( can_cancel
)
1616 if not gpodder
.ui
.fremantle
:
1617 self
.item_cancel_download
.set_sensitive(can_cancel
)
1618 self
.itemDownloadSelected
.set_sensitive(can_download
)
1619 self
.itemOpenSelected
.set_sensitive(can_play
)
1620 self
.itemPlaySelected
.set_sensitive(can_play
)
1621 self
.itemDeleteSelected
.set_sensitive(can_play
and not can_download
)
1622 self
.item_toggle_played
.set_sensitive(can_play
)
1623 self
.item_toggle_lock
.set_sensitive(can_play
)
1624 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
1625 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
1627 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
1629 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1630 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1632 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1633 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1635 def episode_new_status_changed(self
, urls
):
1636 self
.update_podcast_list_model()
1637 self
.update_episode_list_icons(urls
)
1639 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
1640 """Update the podcast list treeview model
1642 If urls is given, it should list the URLs of each
1643 podcast that has to be updated in the list.
1645 If selected is True, only update the model contents
1646 for the currently-selected podcast - nothing more.
1648 The caller can optionally specify "select_url",
1649 which is the URL of the podcast that is to be
1650 selected in the list after the update is complete.
1651 This only works if the podcast list has to be
1652 reloaded; i.e. something has been added or removed
1653 since the last update of the podcast list).
1655 selection
= self
.treeChannels
.get_selection()
1656 model
, iter = selection
.get_selected()
1659 # very cheap! only update selected channel
1660 if iter is not None:
1661 self
.podcast_list_model
.update_by_filter_iter(iter)
1662 elif not self
.channel_list_changed
:
1663 # we can keep the model, but have to update some
1665 # still cheaper than reloading the whole list
1666 self
.podcast_list_model
.update_all()
1668 # ok, we got a bunch of urls to update
1669 self
.podcast_list_model
.update_by_urls(urls
)
1671 if model
and iter and select_url
is None:
1672 # Get the URL of the currently-selected podcast
1673 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
1675 # Update the podcast list model with new channels
1676 self
.podcast_list_model
.set_channels(self
.channels
)
1679 selected_iter
= model
.get_iter_first()
1680 # Find the previously-selected URL in the new
1681 # model if we have an URL (else select first)
1682 if select_url
is not None:
1683 pos
= model
.get_iter_first()
1684 while pos
is not None:
1685 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
1686 if url
== select_url
:
1689 pos
= model
.iter_next(pos
)
1691 if not gpodder
.ui
.fremantle
:
1692 if selected_iter
is not None:
1693 selection
.select_iter(selected_iter
)
1694 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1696 log('Cannot select podcast in list', traceback
=True, sender
=self
)
1697 self
.channel_list_changed
= False
1699 def episode_is_downloading(self
, episode
):
1700 """Returns True if the given episode is being downloaded at the moment"""
1704 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
1706 def update_episode_list_model(self
):
1707 if self
.channels
and self
.active_channel
is not None:
1708 if gpodder
.ui
.diablo
:
1709 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, _('Loading episodes'))
1713 if gpodder
.ui
.fremantle
:
1714 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
1716 self
.currently_updating
= True
1717 self
.episode_list_model
.clear()
1718 def do_update_episode_list_model():
1719 self
.episode_list_model
.add_from_channel(\
1720 self
.active_channel
, \
1721 self
.episode_is_downloading
, \
1722 self
.config
.episode_list_descriptions \
1723 and gpodder
.ui
.desktop
)
1725 def on_episode_list_model_updated():
1726 if banner
is not None:
1728 if gpodder
.ui
.fremantle
:
1729 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
1730 self
.treeAvailable
.columns_autosize()
1731 self
.currently_updating
= False
1732 self
.play_or_download()
1733 util
.idle_add(on_episode_list_model_updated
)
1734 threading
.Thread(target
=do_update_episode_list_model
).start()
1736 self
.episode_list_model
.clear()
1738 def offer_new_episodes(self
, channels
=None):
1739 new_episodes
= self
.get_new_episodes(channels
)
1741 self
.new_episodes_show(new_episodes
)
1745 def add_podcast_list(self
, urls
, auth_tokens
=None):
1746 """Subscribe to a list of podcast given their URLs
1748 If auth_tokens is given, it should be a dictionary
1749 mapping URLs to (username, password) tuples."""
1751 if auth_tokens
is None:
1754 # Sort and split the URL list into five buckets
1755 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
1756 for input_url
in urls
:
1757 url
= util
.normalize_feed_url(input_url
)
1759 # Fail this one because the URL is not valid
1760 failed
.append(input_url
)
1761 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
1762 # A podcast already exists in the list for this URL
1763 existing
.append(url
)
1765 # This URL has survived the first round - queue for add
1767 if url
!= input_url
and input_url
in auth_tokens
:
1768 auth_tokens
[url
] = auth_tokens
[input_url
]
1773 progress
= ProgressIndicator(_('Adding podcasts'), \
1774 _('Please wait while episode information is downloaded.'), \
1775 parent
=self
.main_window
)
1777 def on_after_update():
1778 progress
.on_finished()
1779 # Report already-existing subscriptions to the user
1781 title
= _('Existing subscriptions skipped')
1782 message
= _('You are already subscribed to these podcasts:') \
1783 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
1784 self
.show_message(message
, title
, widget
=self
.treeChannels
)
1786 # Report subscriptions that require authentication
1790 title
= _('Podcast requires authentication')
1791 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
1792 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
1794 retry_podcasts
[url
] = auth_tokens
1796 # Stop asking the user for more login data
1799 error_messages
[url
] = _('Authentication failed')
1803 # If we have authentication data to retry, do so here
1805 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
1807 # Report website redirections
1808 for url
in redirections
:
1809 title
= _('Website redirection detected')
1810 message
= _('The URL %s redirects to %s.') \
1811 + '\n\n' + _('Do you want to visit the website now?')
1812 message
= message
% (url
, redirections
[url
])
1813 if self
.show_confirmation(message
, title
):
1814 util
.open_website(error
.data
)
1818 # Report failed subscriptions to the user
1820 title
= _('Could not add some podcasts')
1821 message
= _('Some podcasts could not be added to your list:') \
1822 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
1823 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
1824 self
.show_message(message
, title
, important
=True)
1826 # If at least one podcast has been added, save and update all
1827 if self
.channel_list_changed
:
1828 self
.save_channels_opml()
1830 # If only one podcast was added, select it after the update
1831 if len(worked
) == 1:
1836 # Update the list of subscribed podcasts
1837 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
1838 self
.update_podcasts_tab()
1840 # Offer to download new episodes
1841 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
1844 # After the initial sorting and splitting, try all queued podcasts
1845 length
= len(queued
)
1846 for index
, url
in enumerate(queued
):
1847 progress
.on_progress(float(index
)/float(length
))
1848 progress
.on_message(url
)
1849 log('QUEUE RUNNER: %s', url
, sender
=self
)
1851 # The URL is valid and does not exist already - subscribe!
1852 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
1853 authentication_tokens
=auth_tokens
.get(url
, None), \
1854 max_episodes
=self
.config
.max_episodes_per_feed
, \
1855 download_dir
=self
.config
.download_dir
)
1858 username
, password
= util
.username_password_from_url(url
)
1859 except ValueError, ve
:
1860 username
, password
= (None, None)
1862 if username
is not None and channel
.username
is None and \
1863 password
is not None and channel
.password
is None:
1864 channel
.username
= username
1865 channel
.password
= password
1868 self
._update
_cover
(channel
)
1869 except feedcore
.AuthenticationRequired
:
1870 if url
in auth_tokens
:
1871 # Fail for wrong authentication data
1872 error_messages
[url
] = _('Authentication failed')
1875 # Queue for login dialog later
1878 except feedcore
.WifiLogin
, error
:
1879 redirections
[url
] = error
.data
1881 error_messages
[url
] = _('Redirection detected')
1883 except Exception, e
:
1884 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
1885 error_messages
[url
] = str(e
)
1889 assert channel
is not None
1890 worked
.append(channel
.url
)
1891 self
.channels
.append(channel
)
1892 self
.channel_list_changed
= True
1893 util
.idle_add(on_after_update
)
1894 threading
.Thread(target
=thread_proc
).start()
1896 def save_channels_opml(self
):
1897 exporter
= opml
.Exporter(gpodder
.subscription_file
)
1898 return exporter
.write(self
.channels
)
1900 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
1902 self
.updating_feed_cache
= False
1904 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
1905 self
.channel_list_changed
= True
1906 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
1908 # Only search for new episodes in podcasts that have been
1909 # updated, not in other podcasts (for single-feed updates)
1910 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
1912 if gpodder
.ui
.fremantle
:
1913 if self
._fremantle
_update
_banner
is not None:
1914 self
._fremantle
_update
_banner
.destroy()
1915 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1916 hildon
.hildon_gtk_window_set_progress_indicator(self
.podcasts_window
.main_window
, False)
1918 self
.new_episodes_show(episodes
)
1920 self
.show_message(_('No new episodes. Please check for new episodes later.'), important
=True)
1924 self
.tray_icon
.set_status()
1926 if self
.feed_cache_update_cancelled
:
1927 # The user decided to abort the feed update
1928 self
.show_update_feeds_buttons()
1930 # Nothing new here - but inform the user
1931 self
.pbFeedUpdate
.set_fraction(1.0)
1932 self
.pbFeedUpdate
.set_text(_('No new episodes'))
1933 self
.feed_cache_update_cancelled
= True
1934 self
.btnCancelFeedUpdate
.show()
1935 self
.btnCancelFeedUpdate
.set_sensitive(True)
1936 if gpodder
.ui
.maemo
:
1937 # btnCancelFeedUpdate is a ToolButton on Maemo
1938 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
1940 # btnCancelFeedUpdate is a normal gtk.Button
1941 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
1943 # New episodes are available
1944 self
.pbFeedUpdate
.set_fraction(1.0)
1945 # Are we minimized and should we auto download?
1946 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
1947 self
.download_episode_list(episodes
)
1948 if len(episodes
) == 1:
1949 title
= _('Downloading one new episode.')
1951 title
= _('Downloading %d new episodes.') % len(episodes
)
1953 if not gpodder
.ui
.fremantle
:
1954 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
1955 self
.show_update_feeds_buttons()
1957 self
.show_update_feeds_buttons()
1958 # New episodes are available and we are not minimized
1959 if not self
.config
.do_not_show_new_episodes_dialog
:
1960 self
.new_episodes_show(episodes
, notification
=True)
1962 if len(episodes
) == 1:
1963 message
= _('One new episode is available for download')
1965 message
= _('%i new episodes are available for download' % len(episodes
))
1967 self
.pbFeedUpdate
.set_text(message
)
1969 def _update_cover(self
, channel
):
1970 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
1971 self
.cover_downloader
.request_cover(channel
)
1973 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
1974 total
= len(channels
)
1976 for updated
, channel
in enumerate(channels
):
1977 if not self
.feed_cache_update_cancelled
:
1979 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1980 if channel
.query_automatic_update() or total
== 1 or not self
.config
.feed_update_skipping
:
1981 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
1983 log('Skipping update of %s (see feed_update_skipping)', channel
.title
, sender
=self
)
1984 self
._update
_cover
(channel
)
1985 except Exception, e
:
1986 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
)
1987 log('Error: %s', str(e
), sender
=self
, traceback
=True)
1989 if self
.feed_cache_update_cancelled
:
1992 if gpodder
.ui
.fremantle
:
1993 if self
._fremantle
_update
_banner
is not None:
1994 progression
= _('%d of %d podcasts updated') % (updated
, total
)
1995 util
.idle_add(self
._fremantle
_update
_banner
.set_text
, progression
)
1996 util
.idle_add(self
._fremantle
_update
_banner
.show
)
1999 # By the time we get here the update may have already been cancelled
2000 if not self
.feed_cache_update_cancelled
:
2001 def update_progress():
2002 progression
= _('Updated %s (%d/%d)') % (channel
.title
, updated
, total
)
2003 self
.pbFeedUpdate
.set_text(progression
)
2005 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2006 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2007 util
.idle_add(update_progress
)
2009 updated_urls
= [c
.url
for c
in channels
]
2010 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2012 def show_update_feeds_buttons(self
):
2013 # Make sure that the buttons for updating feeds
2014 # appear - this should happen after a feed update
2015 if gpodder
.ui
.maemo
:
2016 self
.btnUpdateSelectedFeed
.show()
2017 self
.toolFeedUpdateProgress
.hide()
2018 self
.btnCancelFeedUpdate
.hide()
2019 self
.btnCancelFeedUpdate
.set_is_important(False)
2020 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2021 self
.toolbarSpacer
.set_expand(True)
2022 self
.toolbarSpacer
.set_draw(False)
2024 self
.hboxUpdateFeeds
.hide()
2025 self
.btnUpdateFeeds
.show()
2026 self
.itemUpdate
.set_sensitive(True)
2027 self
.itemUpdateChannel
.set_sensitive(True)
2029 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2030 if not self
.feed_cache_update_cancelled
:
2031 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2032 self
.feed_cache_update_cancelled
= True
2033 self
.btnCancelFeedUpdate
.set_sensitive(False)
2035 self
.show_update_feeds_buttons()
2037 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2038 if self
.updating_feed_cache
:
2041 if not force_update
:
2042 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2043 self
.channel_list_changed
= True
2044 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2047 self
.updating_feed_cache
= True
2049 if channels
is None:
2050 channels
= self
.channels
2052 if gpodder
.ui
.fremantle
:
2053 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2054 hildon
.hildon_gtk_window_set_progress_indicator(self
.podcasts_window
.main_window
, True)
2055 self
._fremantle
_update
_banner
= hildon
.hildon_banner_show_animation(self
.main_window
, \
2056 '', _('Updating podcast feeds'))
2058 self
.itemUpdate
.set_sensitive(False)
2059 self
.itemUpdateChannel
.set_sensitive(False)
2062 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2064 if len(channels
) == 1:
2065 text
= _('Updating "%s"...') % channels
[0].title
2067 text
= _('Updating %d feeds...') % len(channels
)
2068 self
.pbFeedUpdate
.set_text(text
)
2069 self
.pbFeedUpdate
.set_fraction(0)
2071 self
.feed_cache_update_cancelled
= False
2072 self
.btnCancelFeedUpdate
.show()
2073 self
.btnCancelFeedUpdate
.set_sensitive(True)
2074 if gpodder
.ui
.maemo
:
2075 self
.toolbarSpacer
.set_expand(False)
2076 self
.toolbarSpacer
.set_draw(True)
2077 self
.btnUpdateSelectedFeed
.hide()
2078 self
.toolFeedUpdateProgress
.show_all()
2080 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2081 self
.hboxUpdateFeeds
.show_all()
2082 self
.btnUpdateFeeds
.hide()
2084 args
= (channels
, select_url_afterwards
)
2085 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2087 def on_gPodder_delete_event(self
, widget
, *args
):
2088 """Called when the GUI wants to close the window
2089 Displays a confirmation dialog (and closes/hides gPodder)
2092 downloading
= self
.download_status_model
.are_downloads_in_progress()
2094 # Only iconify if we are using the window's "X" button,
2095 # but not when we are using "Quit" in the menu or toolbar
2096 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'):
2097 self
.iconify_main_window()
2098 elif self
.config
.on_quit_ask
or downloading
:
2099 if gpodder
.ui
.maemo
:
2100 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2102 self
.close_gpodder()
2105 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2106 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2107 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2109 title
= _('Quit gPodder')
2111 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2113 message
= _('Do you really want to quit gPodder now?')
2115 dialog
.set_title(title
)
2116 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2118 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2119 dialog
.vbox
.pack_start(cb_ask
)
2122 quit_button
.grab_focus()
2123 result
= dialog
.run()
2126 if result
== gtk
.RESPONSE_CLOSE
:
2127 if not downloading
and cb_ask
.get_active() == True:
2128 self
.config
.on_quit_ask
= False
2129 self
.close_gpodder()
2131 self
.close_gpodder()
2135 def close_gpodder(self
):
2136 """ clean everything and exit properly
2139 if self
.save_channels_opml():
2140 if self
.config
.my_gpodder_autoupload
:
2141 log('Uploading to my.gpodder.org on close', sender
=self
)
2142 util
.idle_add(self
.on_upload_to_mygpo
, None)
2144 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2148 if self
.tray_icon
is not None:
2149 self
.tray_icon
.set_visible(False)
2151 # Notify all tasks to to carry out any clean-up actions
2152 self
.download_status_model
.tell_all_tasks_to_quit()
2154 while gtk
.events_pending():
2155 gtk
.main_iteration(False)
2162 def get_old_episodes(self
):
2164 for channel
in self
.channels
:
2165 for episode
in channel
.get_downloaded_episodes():
2166 if episode
.age_in_days() > self
.config
.episode_old_age
and \
2167 not episode
.is_locked
and episode
.is_played
:
2168 episodes
.append(episode
)
2171 def delete_episode_list(self
, episodes
, confirm
=True):
2175 count
= len(episodes
)
2178 episode
= episodes
[0]
2179 if episode
.is_locked
:
2180 title
= _('%s is locked') % saxutils
.escape(episode
.title
)
2181 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2182 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2185 title
= _('Remove %s?') % saxutils
.escape(episode
.title
)
2186 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.")
2188 title
= _('Remove %d episodes?') % count
2189 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.')
2191 locked_count
= sum(int(e
.is_locked
) for e
in episodes
if e
.is_locked
is not None)
2193 if count
== locked_count
:
2194 title
= _('Episodes are locked')
2195 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2196 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2198 elif locked_count
> 0:
2199 title
= _('Remove %d out of %d episodes?') % (count
-locked_count
, count
)
2200 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.')
2202 if confirm
and not self
.show_confirmation(message
, title
):
2205 episode_urls
= set()
2206 channel_urls
= set()
2207 for episode
in episodes
:
2208 if episode
.is_locked
:
2209 log('Not deleting episode (is locked): %s', episode
.title
)
2211 log('Deleting episode: %s', episode
.title
)
2212 episode
.delete_from_disk()
2213 episode_urls
.add(episode
.url
)
2214 channel_urls
.add(episode
.channel
.url
)
2216 # Tell the shownotes window that we have removed the episode
2217 if self
.episode_shownotes_window
is not None and \
2218 self
.episode_shownotes_window
.episode
is not None and \
2219 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2220 self
.episode_shownotes_window
._download
_status
_changed
(None)
2222 # Episodes have been deleted - persist the database
2225 self
.update_episode_list_icons(episode_urls
)
2226 self
.update_podcast_list_model(channel_urls
)
2227 self
.play_or_download()
2229 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2230 if gpodder
.ui
.maemo
:
2232 ('maemo_remove_markup', None, None, _('Episode')),
2236 ('title_markup', None, None, _('Episode')),
2237 ('channel_prop', None, None, _('Podcast')),
2238 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2239 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2240 ('played_prop', None, None, _('Status')),
2241 ('age_prop', None, None, _('Downloaded')),
2244 selection_buttons
= {
2245 _('Select played'): lambda episode
: episode
.is_played
,
2246 _('Select older than %d days') % self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2249 instructions
= _('Select the episodes you want to delete:')
2253 for channel
in self
.channels
:
2254 for episode
in channel
.get_downloaded_episodes():
2255 if not episode
.is_locked
:
2256 episodes
.append(episode
)
2257 selected
.append(episode
.is_played
)
2259 gPodderEpisodeSelector(self
.gPodder
, title
= _('Remove old episodes'), instructions
= instructions
, \
2260 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2261 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2262 selection_buttons
= selection_buttons
, _config
=self
.config
)
2264 def on_selected_episodes_status_changed(self
):
2265 self
.update_episode_list_icons(selected
=True)
2266 self
.update_podcast_list_model(selected
=True)
2269 def mark_selected_episodes_new(self
):
2270 for episode
in self
.get_selected_episodes():
2272 self
.on_selected_episodes_status_changed()
2274 def mark_selected_episodes_old(self
):
2275 for episode
in self
.get_selected_episodes():
2277 self
.on_selected_episodes_status_changed()
2279 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2280 for episode
in self
.get_selected_episodes():
2282 episode
.mark(is_played
=not episode
.is_played
)
2284 episode
.mark(is_played
=new_value
)
2285 self
.on_selected_episodes_status_changed()
2287 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2288 for episode
in self
.get_selected_episodes():
2290 episode
.mark(is_locked
=not episode
.is_locked
)
2292 episode
.mark(is_locked
=new_value
)
2293 self
.on_selected_episodes_status_changed()
2295 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2296 if self
.active_channel
is None:
2299 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2300 self
.active_channel
.update_channel_lock()
2302 for episode
in self
.active_channel
.get_all_episodes():
2303 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
2305 self
.update_podcast_list_model(selected
=True)
2306 self
.update_episode_list_icons(all
=True)
2308 def send_subscriptions(self
):
2310 subprocess
.Popen(['xdg-email', '--subject', _('My podcast subscriptions'),
2311 '--attach', gpodder
.subscription_file
])
2317 def on_item_email_subscriptions_activate(self
, widget
):
2318 if not self
.channels
:
2319 self
.show_message(_('Your subscription list is empty. Add some podcasts first.'), _('Could not send list'), widget
=self
.treeChannels
)
2320 elif not self
.send_subscriptions():
2321 self
.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'), important
=True)
2323 def on_itemUpdateChannel_activate(self
, widget
=None):
2324 if self
.active_channel
is None:
2325 title
= _('No podcast selected')
2326 message
= _('Please select a podcast in the podcasts list to update.')
2327 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2330 self
.update_feed_cache(channels
=[self
.active_channel
])
2332 def on_itemUpdate_activate(self
, widget
=None):
2334 self
.update_feed_cache()
2336 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
)
2338 def download_episode_list_paused(self
, episodes
):
2339 self
.download_episode_list(episodes
, True)
2341 def download_episode_list(self
, episodes
, add_paused
=False):
2342 for episode
in episodes
:
2343 log('Downloading episode: %s', episode
.title
, sender
= self
)
2344 if not episode
.was_downloaded(and_exists
=True):
2346 for task
in self
.download_tasks_seen
:
2347 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
2348 self
.download_queue_manager
.add_task(task
)
2349 self
.enable_download_list_update()
2357 task
= download
.DownloadTask(episode
, self
.config
)
2358 except Exception, e
:
2359 self
.show_message(_('Download error while downloading %s:\n\n%s') % (episode
.title
, str(e
)), _('Download error'), important
=True)
2360 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
2364 task
.status
= task
.PAUSED
2366 self
.download_queue_manager
.add_task(task
)
2368 self
.download_status_model
.register_task(task
)
2369 self
.enable_download_list_update()
2371 def cancel_task_list(self
, tasks
):
2376 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
2377 task
.status
= task
.CANCELLED
2378 elif task
.status
== task
.PAUSED
:
2379 task
.status
= task
.CANCELLED
2380 # Call run, so the partial file gets deleted
2383 self
.update_episode_list_icons([task
.url
for task
in tasks
])
2384 self
.play_or_download()
2386 # Update the tab title and downloads list
2387 self
.update_downloads_list()
2389 def new_episodes_show(self
, episodes
, notification
=False):
2390 if gpodder
.ui
.maemo
:
2392 ('maemo_markup', None, None, _('Episode')),
2394 show_notification
= notification
2397 ('title_markup', None, None, _('Episode')),
2398 ('channel_prop', None, None, _('Podcast')),
2399 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2400 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2402 show_notification
= False
2404 instructions
= _('Select the episodes you want to download:')
2406 gPodderEpisodeSelector(self
.gPodder
, title
=_('New episodes available'), instructions
=instructions
, \
2407 episodes
=episodes
, columns
=columns
, selected_default
=True, \
2408 stock_ok_button
= 'gpodder-download', \
2409 callback
=self
.download_episode_list
, \
2410 remove_callback
=lambda e
: e
.mark_old(), \
2411 remove_action
=_('Mark as old'), \
2412 remove_finished
=self
.episode_new_status_changed
, \
2413 _config
=self
.config
, \
2414 show_notification
=show_notification
)
2416 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
2417 if not self
.offer_new_episodes():
2418 self
.show_message(_('Please check for new episodes later.'), \
2419 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
2421 def get_new_episodes(self
, channels
=None):
2422 if channels
is None:
2423 channels
= self
.channels
2425 for channel
in channels
:
2426 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
2427 episodes
.append(episode
)
2431 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
2432 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
2433 # The sync process might have updated the status of episodes,
2434 # therefore persist the database here to avoid losing data
2437 def on_cleanup_ipod_activate(self
, widget
, *args
):
2438 self
.sync_ui
.on_cleanup_device()
2440 def on_manage_device_playlist(self
, widget
):
2441 self
.sync_ui
.on_manage_device_playlist()
2443 def show_hide_tray_icon(self
):
2444 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
2445 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
2446 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
2447 self
.tray_icon
.set_visible(False)
2449 self
.tray_icon
= None
2451 if self
.config
.minimize_to_tray
and self
.tray_icon
:
2452 self
.tray_icon
.set_visible(self
.is_iconified())
2453 elif self
.tray_icon
:
2454 self
.tray_icon
.set_visible(True)
2456 def on_itemShowToolbar_activate(self
, widget
):
2457 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
2459 def on_itemShowDescription_activate(self
, widget
):
2460 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
2462 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
2463 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
2464 if self
.config
.podcast_list_hide_boring
:
2465 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2467 self
.podcast_list_model
.set_view_mode(-1)
2469 def on_item_view_podcasts_changed(self
, radioaction
, current
):
2471 if current
== self
.item_view_podcasts_all
:
2472 self
.podcast_list_model
.set_view_mode(-1)
2473 elif current
== self
.item_view_podcasts_downloaded
:
2474 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2475 elif current
== self
.item_view_podcasts_unplayed
:
2476 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2478 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
2480 def on_item_view_episodes_changed(self
, radioaction
, current
):
2481 if current
== self
.item_view_episodes_all
:
2482 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
2483 elif current
== self
.item_view_episodes_undeleted
:
2484 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
2485 elif current
== self
.item_view_episodes_downloaded
:
2486 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2487 elif current
== self
.item_view_episodes_unplayed
:
2488 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2490 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
2492 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
2493 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2495 def update_item_device( self
):
2496 if not gpodder
.ui
.fremantle
:
2497 if self
.config
.device_type
!= 'none':
2498 self
.itemDevice
.set_visible(True)
2499 self
.itemDevice
.label
= self
.get_device_name()
2501 self
.itemDevice
.set_visible(False)
2503 def properties_closed( self
):
2504 self
.show_hide_tray_icon()
2505 self
.update_item_device()
2506 if gpodder
.ui
.maemo
:
2507 selection
= self
.treeAvailable
.get_selection()
2508 if self
.config
.maemo_enable_gestures
or \
2509 self
.config
.enable_fingerscroll
:
2510 selection
.set_mode(gtk
.SELECTION_SINGLE
)
2512 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
2514 def on_itemPreferences_activate(self
, widget
, *args
):
2515 gPodderPreferences(self
.gPodder
, _config
=self
.config
, \
2516 callback_finished
=self
.properties_closed
, \
2517 user_apps_reader
=self
.user_apps_reader
)
2519 def on_itemDependencies_activate(self
, widget
):
2520 gPodderDependencyManager(self
.gPodder
)
2522 def require_my_gpodder_authentication(self
):
2523 if not self
.config
.my_gpodder_username
or not self
.config
.my_gpodder_password
:
2524 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'))
2525 if success
and authentication
[0] and authentication
[1]:
2526 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
= authentication
2533 def my_gpodder_offer_autoupload(self
):
2534 if not self
.config
.my_gpodder_autoupload
:
2535 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')):
2536 self
.config
.my_gpodder_autoupload
= True
2538 def on_download_from_mygpo(self
, widget
):
2539 if self
.require_my_gpodder_authentication():
2540 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2541 opml_data
= client
.download_subscriptions()
2542 if len(opml_data
) > 0:
2543 fp
= open(gpodder
.subscription_file
, 'w')
2546 (added
, skipped
) = (0, 0)
2547 i
= opml
.Importer(gpodder
.subscription_file
)
2549 existing
= [c
.url
for c
in self
.channels
]
2550 urls
= [item
['url'] for item
in i
.items
if item
['url'] not in existing
]
2552 skipped
= len(i
.items
) - len(urls
)
2555 self
.add_podcast_list(urls
)
2557 self
.my_gpodder_offer_autoupload()
2559 self
.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added
, skipped
), _('Result of subscription download'), widget
=self
.treeChannels
)
2560 elif widget
is not None:
2561 self
.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget
=self
.treeChannels
)
2563 self
.config
.my_gpodder_password
= ''
2564 self
.on_download_from_mygpo(widget
)
2566 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2568 def on_upload_to_mygpo(self
, widget
):
2569 if self
.require_my_gpodder_authentication():
2570 client
= my
.MygPodderClient(self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2571 self
.save_channels_opml()
2572 success
, messages
= client
.upload_subscriptions(gpodder
.subscription_file
)
2573 if widget
is not None:
2575 self
.show_message('\n'.join(messages
), _('Results of upload'), important
=True)
2576 self
.config
.my_gpodder_password
= ''
2577 self
.on_upload_to_mygpo(widget
)
2579 self
.my_gpodder_offer_autoupload()
2580 self
.show_message('\n'.join(messages
), _('Results of upload'), widget
=self
.treeChannels
)
2582 log('Upload to my.gpodder.org failed, but widget is None!', sender
=self
)
2583 elif widget
is not None:
2584 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2586 def on_itemAddChannel_activate(self
, widget
, *args
):
2587 gPodderAddPodcast(self
.gPodder
, \
2588 add_urls_callback
=self
.add_podcast_list
)
2590 def on_itemEditChannel_activate(self
, widget
, *args
):
2591 if self
.active_channel
is None:
2592 title
= _('No podcast selected')
2593 message
= _('Please select a podcast in the podcasts list to edit.')
2594 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2597 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
2598 gPodderChannel(self
.main_window
, \
2599 channel
=self
.active_channel
, \
2600 callback_closed
=callback_closed
, \
2601 cover_downloader
=self
.cover_downloader
)
2603 def on_itemRemoveChannel_activate(self
, widget
, *args
):
2604 if self
.active_channel
is None:
2605 title
= _('No podcast selected')
2606 message
= _('Please select a podcast in the podcasts list to remove.')
2607 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2611 if gpodder
.ui
.desktop
:
2612 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2613 dialog
.add_button(gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
2614 dialog
.add_button(gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
2616 title
= _('Remove podcast and episodes?')
2617 message
= _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils
.escape(self
.active_channel
.title
)
2619 dialog
.set_title(title
)
2620 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2622 cb_ask
= gtk
.CheckButton(_('Do not delete my downloaded episodes'))
2623 dialog
.vbox
.pack_start(cb_ask
)
2625 result
= (dialog
.run() == gtk
.RESPONSE_YES
)
2626 keep_episodes
= cb_ask
.get_active()
2628 elif gpodder
.ui
.diablo
:
2629 result
= self
.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2630 keep_episodes
= False
2631 elif gpodder
.ui
.fremantle
:
2633 keep_episodes
= False
2636 # delete downloaded episodes only if checkbox is unchecked
2638 log('Not removing downloaded episodes', sender
=self
)
2640 self
.active_channel
.remove_downloaded()
2642 # Clean up downloads and download directories
2643 self
.clean_up_downloads()
2645 # cancel any active downloads from this channel
2646 for episode
in self
.active_channel
.get_all_episodes():
2647 self
.download_status_model
.cancel_by_url(episode
.url
)
2649 # get the URL of the podcast we want to select next
2650 position
= self
.channels
.index(self
.active_channel
)
2651 if position
== len(self
.channels
)-1:
2652 # this is the last podcast, so select the URL
2653 # of the item before this one (i.e. the "new last")
2654 select_url
= self
.channels
[position
-1].url
2656 # there is a podcast after the deleted one, so
2657 # we simply select the one that comes after it
2658 select_url
= self
.channels
[position
+1].url
2660 title
= self
.active_channel
.title
2662 # Remove the channel
2663 self
.active_channel
.delete(purge
=not keep_episodes
)
2664 self
.channels
.remove(self
.active_channel
)
2665 self
.channel_list_changed
= True
2666 self
.save_channels_opml()
2668 if gpodder
.ui
.fremantle
:
2669 self
.show_message(_('Podcast removed: %s') % title
)
2671 # Re-load the channels and select the desired new channel
2672 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
2674 log('There has been an error removing the channel.', traceback
=True, sender
=self
)
2675 self
.update_podcasts_tab()
2677 def get_opml_filter(self
):
2678 filter = gtk
.FileFilter()
2679 filter.add_pattern('*.opml')
2680 filter.add_pattern('*.xml')
2681 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2684 def on_item_import_from_file_activate(self
, widget
, filename
=None):
2685 if filename
is None:
2686 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2687 # FIXME: Hildonization on Fremantle
2688 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2689 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2690 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2691 elif gpodder
.ui
.diablo
:
2692 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2693 dlg
.set_filter(self
.get_opml_filter())
2694 response
= dlg
.run()
2696 if response
== gtk
.RESPONSE_OK
:
2697 filename
= dlg
.get_filename()
2700 if filename
is not None:
2701 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
2702 custom_title
=_('Import podcasts from OPML file'), \
2703 add_urls_callback
=self
.add_podcast_list
, \
2704 hide_url_entry
=True)
2705 dir.download_opml_file(filename
)
2707 def on_itemExportChannels_activate(self
, widget
, *args
):
2708 if not self
.channels
:
2709 title
= _('Nothing to export')
2710 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2711 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2714 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2715 # FIXME: Hildonization on Fremantle
2716 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
2717 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2718 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
2719 elif gpodder
.ui
.diablo
:
2720 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
2721 dlg
.set_filter(self
.get_opml_filter())
2722 response
= dlg
.run()
2723 if response
== gtk
.RESPONSE_OK
:
2724 filename
= dlg
.get_filename()
2726 exporter
= opml
.Exporter( filename
)
2727 if exporter
.write(self
.channels
):
2728 if len(self
.channels
) == 1:
2729 title
= _('One subscription exported')
2731 title
= _('%d subscriptions exported') % len(self
.channels
)
2732 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
2734 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
2738 def on_itemImportChannels_activate(self
, widget
, *args
):
2739 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
2740 add_urls_callback
=self
.add_podcast_list
)
2741 if not gpodder
.ui
.fremantle
:
2742 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
2744 def on_homepage_activate(self
, widget
, *args
):
2745 util
.open_website(gpodder
.__url
__)
2747 def on_wiki_activate(self
, widget
, *args
):
2748 util
.open_website('http://wiki.gpodder.org/')
2750 def on_bug_tracker_activate(self
, widget
, *args
):
2751 if gpodder
.ui
.maemo
:
2752 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2754 util
.open_website('http://bugs.gpodder.org/')
2756 def on_shop_activate(self
, widget
, *args
):
2757 util
.open_website('http://gpodder.org/shop')
2759 def on_wishlist_activate(self
, widget
, *args
):
2760 util
.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2762 def on_itemAbout_activate(self
, widget
, *args
):
2763 dlg
= gtk
.AboutDialog()
2764 dlg
.set_name('gPodder')
2765 dlg
.set_version(gpodder
.__version
__)
2766 dlg
.set_copyright(gpodder
.__copyright
__)
2767 dlg
.set_website(gpodder
.__url
__)
2768 dlg
.set_translator_credits( _('translator-credits'))
2769 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
2771 if gpodder
.ui
.desktop
:
2772 # For the "GUI" version, we add some more
2773 # items to the about dialog (credits and logo)
2776 'Thomas Perl <thpinfo.com>',
2779 if os
.path
.exists(gpodder
.credits_file
):
2780 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
2781 app_authors
+= ['', _('Patches, bug reports and donations by:')]
2782 app_authors
+= credits
2784 dlg
.set_authors(app_authors
)
2786 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
2788 dlg
.set_logo_icon_name('gpodder')
2792 def on_wNotebook_switch_page(self
, widget
, *args
):
2794 if gpodder
.ui
.maemo
:
2795 self
.tool_downloads
.set_active(page_num
== 1)
2796 page
= self
.wNotebook
.get_nth_page(page_num
)
2797 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
2798 if page_num
== 0 and self
.active_channel
is not None:
2799 self
.set_title(self
.active_channel
.title
)
2801 self
.set_title(tab_label
)
2803 self
.play_or_download()
2804 self
.menuChannels
.set_sensitive(True)
2805 self
.menuSubscriptions
.set_sensitive(True)
2806 # The message area in the downloads tab should be hidden
2807 # when the user switches away from the downloads tab
2808 if self
.message_area
is not None:
2809 self
.message_area
.hide()
2810 self
.message_area
= None
2812 self
.menuChannels
.set_sensitive(False)
2813 self
.menuSubscriptions
.set_sensitive(False)
2814 if gpodder
.ui
.desktop
:
2815 self
.toolDownload
.set_sensitive(False)
2816 self
.toolPlay
.set_sensitive(False)
2817 self
.toolTransfer
.set_sensitive(False)
2818 self
.toolCancel
.set_sensitive(False)
2820 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
2821 # double-click action of the podcast list or enter
2822 self
.treeChannels
.set_cursor(path
)
2824 def on_treeChannels_cursor_changed(self
, widget
, *args
):
2825 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
2827 if model
is not None and iter is not None:
2828 old_active_channel
= self
.active_channel
2829 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
2831 if self
.active_channel
== old_active_channel
:
2834 if gpodder
.ui
.maemo
:
2835 self
.set_title(self
.active_channel
.title
)
2836 self
.itemEditChannel
.set_visible(True)
2837 self
.itemRemoveChannel
.set_visible(True)
2839 self
.active_channel
= None
2840 self
.itemEditChannel
.set_visible(False)
2841 self
.itemRemoveChannel
.set_visible(False)
2843 self
.update_episode_list_model()
2845 def on_btnEditChannel_clicked(self
, widget
, *args
):
2846 self
.on_itemEditChannel_activate( widget
, args
)
2848 def get_selected_episodes(self
):
2849 """Get a list of selected episodes from treeAvailable"""
2850 selection
= self
.treeAvailable
.get_selection()
2851 model
, paths
= selection
.get_selected_rows()
2853 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
2856 def on_transfer_selected_episodes(self
, widget
):
2857 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
2859 def on_playback_selected_episodes(self
, widget
):
2860 self
.playback_episodes(self
.get_selected_episodes())
2862 def on_shownotes_selected_episodes(self
, widget
):
2863 episodes
= self
.get_selected_episodes()
2865 episode
= episodes
.pop(0)
2866 self
.show_episode_shownotes(episode
)
2868 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
2870 def on_download_selected_episodes(self
, widget
):
2871 episodes
= self
.get_selected_episodes()
2872 self
.download_episode_list(episodes
)
2873 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
2874 self
.play_or_download()
2876 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
2877 """Double-click/enter action handler for treeAvailable"""
2878 # We should only have one one selected as it was double clicked!
2879 e
= self
.get_selected_episodes()[0]
2881 if (self
.config
.double_click_episode_action
== 'download'):
2882 # If the episode has already been downloaded and exists then play it
2883 if e
.was_downloaded(and_exists
=True):
2884 self
.playback_episodes(self
.get_selected_episodes())
2885 # else download it if it is not already downloading
2886 elif not self
.episode_is_downloading(e
):
2887 self
.download_episode_list([e
])
2888 self
.update_episode_list_icons([e
.url
])
2889 self
.play_or_download()
2890 elif (self
.config
.double_click_episode_action
== 'stream'):
2891 # If we happen to have downloaded this episode simple play it
2892 if e
.was_downloaded(and_exists
=True):
2893 self
.playback_episodes(self
.get_selected_episodes())
2894 # else if streaming is possible stream it
2895 elif self
.streaming_possible():
2896 self
.playback_episodes(self
.get_selected_episodes())
2898 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
2899 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
2901 # default action is to display show notes
2902 self
.on_shownotes_selected_episodes(widget
)
2904 def show_episode_shownotes(self
, episode
):
2905 if self
.episode_shownotes_window
is None:
2906 log('First-time use of episode window --- creating', sender
=self
)
2907 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
2908 _download_episode_list
=self
.download_episode_list
, \
2909 _playback_episodes
=self
.playback_episodes
, \
2910 _delete_episode_list
=self
.delete_episode_list
, \
2911 _episode_list_status_changed
=self
.episode_list_status_changed
, \
2912 _cancel_task_list
=self
.cancel_task_list
, \
2913 _episode_is_downloading
=self
.episode_is_downloading
)
2914 self
.episode_shownotes_window
.show(episode
)
2915 if self
.episode_is_downloading(episode
):
2916 self
.update_downloads_list()
2918 def auto_update_procedure(self
, first_run
=False):
2919 log('auto_update_procedure() got called', sender
=self
)
2920 if not first_run
and self
.config
.auto_update_feeds
and self
.is_iconified():
2921 self
.update_feed_cache(force_update
=True)
2923 next_update
= 60*1000*self
.config
.auto_update_frequency
2924 gobject
.timeout_add(next_update
, self
.auto_update_procedure
)
2927 def on_treeDownloads_row_activated(self
, widget
, *args
):
2928 # Use the standard way of working on the treeview
2929 selection
= self
.treeDownloads
.get_selection()
2930 (model
, paths
) = selection
.get_selected_rows()
2931 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
2933 for tree_row_reference
, task
in selected_tasks
:
2934 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
2935 task
.status
= task
.PAUSED
2936 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
2937 self
.download_queue_manager
.add_task(task
)
2938 self
.enable_download_list_update()
2939 elif task
.status
== task
.DONE
:
2940 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
2942 self
.play_or_download()
2944 # Update the tab title and downloads list
2945 self
.update_downloads_list()
2947 def on_item_cancel_download_activate(self
, widget
):
2948 if self
.wNotebook
.get_current_page() == 0:
2949 selection
= self
.treeAvailable
.get_selection()
2950 (model
, paths
) = selection
.get_selected_rows()
2951 urls
= [model
.get_value(model
.get_iter(path
), \
2952 self
.episode_list_model
.C_URL
) for path
in paths
]
2953 selected_tasks
= [task
for task
in self
.download_tasks_seen \
2954 if task
.url
in urls
]
2956 selection
= self
.treeDownloads
.get_selection()
2957 (model
, paths
) = selection
.get_selected_rows()
2958 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
2959 self
.download_status_model
.C_TASK
) for path
in paths
]
2960 self
.cancel_task_list(selected_tasks
)
2962 def on_btnCancelAll_clicked(self
, widget
, *args
):
2963 self
.cancel_task_list(self
.download_tasks_seen
)
2965 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
2966 if self
.wNotebook
.get_current_page() == 1:
2967 # Downloads tab visibile - skip (for now)
2970 episodes
= self
.get_selected_episodes()
2971 self
.delete_episode_list(episodes
)
2973 def on_key_press(self
, widget
, event
):
2974 # Allow tab switching with Ctrl + PgUp/PgDown
2975 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
2976 if event
.keyval
== gtk
.keysyms
.Page_Up
:
2977 self
.wNotebook
.prev_page()
2979 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
2980 self
.wNotebook
.next_page()
2983 # After this code we only handle Maemo hardware keys,
2984 # so if we are not a Maemo app, we don't do anything
2985 if not gpodder
.ui
.maemo
:
2989 if event
.keyval
== gtk
.keysyms
.F7
: #plus
2991 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
2994 if diff
!= 0 and not self
.currently_updating
:
2995 selection
= self
.treeChannels
.get_selection()
2996 (model
, iter) = selection
.get_selected()
2997 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
2998 selection
.select_path(new_path
)
2999 self
.treeChannels
.set_cursor(new_path
)
3004 def on_iconify(self
):
3006 self
.gPodder
.set_skip_taskbar_hint(True)
3007 if self
.config
.minimize_to_tray
:
3008 self
.tray_icon
.set_visible(True)
3010 self
.gPodder
.set_skip_taskbar_hint(False)
3012 def on_uniconify(self
):
3014 self
.gPodder
.set_skip_taskbar_hint(False)
3015 if self
.config
.minimize_to_tray
:
3016 self
.tray_icon
.set_visible(False)
3018 self
.gPodder
.set_skip_taskbar_hint(False)
3020 def uniconify_main_window(self
):
3021 if self
.is_iconified():
3022 self
.gPodder
.present()
3024 def iconify_main_window(self
):
3025 if not self
.is_iconified():
3026 self
.gPodder
.iconify()
3028 def update_podcasts_tab(self
):
3029 if len(self
.channels
):
3030 if gpodder
.ui
.fremantle
:
3031 self
.button_podcasts
.set_value(_('%d subscriptions') % len(self
.channels
))
3033 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3035 if gpodder
.ui
.fremantle
:
3036 self
.button_podcasts
.set_value(_('No subscriptions'))
3038 self
.label2
.set_text(_('Podcasts'))
3040 @dbus.service
.method(gpodder
.dbus_interface
)
3041 def show_gui_window(self
):
3042 self
.gPodder
.present()
3044 @dbus.service
.method(gpodder
.dbus_interface
)
3045 def subscribe_to_url(self
, url
):
3046 gPodderAddPodcast(self
.gPodder
,
3047 add_urls_callback
=self
.add_podcast_list
,
3050 @dbus.service
.method(gpodder
.dbus_interface
)
3051 def mark_episode_played(self
, filename
):
3052 if filename
is None:
3055 for channel
in self
.channels
:
3056 for episode
in channel
.get_all_episodes():
3057 fn
= episode
.local_filename(create
=False, check_only
=True)
3059 episode
.mark(is_played
=True)
3061 self
.update_episode_list_icons([episode
.url
])
3062 self
.update_podcast_list_model([episode
.channel
.url
])
3068 def main(options
=None):
3069 gobject
.threads_init()
3070 gobject
.set_application_name('gPodder')
3072 if gpodder
.ui
.diablo
:
3073 # Try to enable the custom icon theme for gPodder on Maemo
3074 settings
= gtk
.settings_get_default()
3075 settings
.set_string_property('gtk-icon-theme-name', \
3076 'gpodder', __file__
)
3078 gtk
.window_set_default_icon_name('gpodder')
3079 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3082 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3083 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3084 except dbus
.exceptions
.DBusException
, dbe
:
3085 log('Warning: Cannot get "on the bus".', traceback
=True)
3086 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3087 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3088 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3089 dlg
.set_title('gPodder')
3094 util
.make_directory(gpodder
.home
)
3095 config
= UIConfig(gpodder
.config_file
)
3097 if gpodder
.ui
.diablo
:
3098 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3099 # folder exists there (allow moving "gpodder" between SD cards or USB)
3100 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3101 if not os
.path
.exists(config
.download_dir
):
3102 log('Downloads might have been moved. Trying to locate them...')
3103 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3104 dir = os
.path
.join(basedir
, 'gpodder')
3105 if os
.path
.exists(dir):
3106 log('Downloads found in: %s', dir)
3107 config
.download_dir
= dir
3110 log('Downloads NOT FOUND in %s', dir)
3112 if config
.enable_fingerscroll
:
3113 BuilderWidget
.use_fingerscroll
= True
3114 elif gpodder
.ui
.fremantle
:
3115 # FIXME: Move download_dir from ~/gPodder-Podcasts to default setting
3118 gp
= gPodder(bus_name
, config
)
3121 if options
.subscribe
:
3122 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)