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