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