Add progress indicator for deleting items (bug 268)
[gpodder.git] / src / gpodder / gui.py
blobf88f54d97ce6ef508e03afb24da74a911c1b3206
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.services import CoverDownloader
82 from gpodder.gtkui.widgets import SimpleMessageArea
83 from gpodder.gtkui.desktopfile import UserAppsReader
85 from gpodder.gtkui.draw import draw_text_box_centered
87 from gpodder.gtkui.interface.common import BuilderWidget
88 from gpodder.gtkui.interface.common import TreeViewHelper
89 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
91 if gpodder.ui.desktop:
92 from gpodder.gtkui.download import DownloadStatusModel
94 from gpodder.gtkui.desktop.sync import gPodderSyncUI
96 from gpodder.gtkui.desktop.channel import gPodderChannel
97 from gpodder.gtkui.desktop.preferences import gPodderPreferences
98 from gpodder.gtkui.desktop.shownotes import gPodderShownotes
99 from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
100 from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
101 from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
102 try:
103 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
104 have_trayicon = True
105 except Exception, exc:
106 log('Warning: Could not import gpodder.trayicon.', traceback=True)
107 log('Warning: This probably means your PyGTK installation is too old!')
108 have_trayicon = False
109 elif gpodder.ui.diablo:
110 from gpodder.gtkui.download import DownloadStatusModel
112 from gpodder.gtkui.maemo.channel import gPodderChannel
113 from gpodder.gtkui.maemo.preferences import gPodderPreferences
114 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
115 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
116 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
117 have_trayicon = False
118 elif gpodder.ui.fremantle:
119 from gpodder.gtkui.frmntl.model import DownloadStatusModel
121 from gpodder.gtkui.maemo.channel import gPodderChannel
122 from gpodder.gtkui.frmntl.preferences import gPodderPreferences
123 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
124 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
125 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
126 from gpodder.gtkui.frmntl.podcasts import gPodderPodcasts
127 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
128 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
129 from gpodder.gtkui.interface.common import Orientation
130 have_trayicon = False
132 from gpodder.gtkui.frmntl.portrait import FremantleRotation
134 from gpodder.gtkui.interface.welcome import gPodderWelcome
135 from gpodder.gtkui.interface.progress import ProgressIndicator
137 if gpodder.ui.maemo:
138 import hildon
140 class gPodder(BuilderWidget, dbus.service.Object):
141 finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear']
143 def __init__(self, bus_name, config):
144 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
145 self.db = Database(gpodder.database_file)
146 self.config = config
147 BuilderWidget.__init__(self, None)
149 def new(self):
150 if gpodder.ui.diablo:
151 import hildon
152 self.app = hildon.Program()
153 self.app.add_window(self.main_window)
154 self.main_window.add_toolbar(self.toolbar)
155 menu = gtk.Menu()
156 for child in self.main_menu.get_children():
157 child.reparent(menu)
158 self.main_window.set_menu(self.set_finger_friendly(menu))
159 self.bluetooth_available = False
160 elif gpodder.ui.fremantle:
161 import hildon
162 self.app = hildon.Program()
163 self.app.add_window(self.main_window)
165 appmenu = hildon.AppMenu()
166 for action in (self.itemUpdate, \
167 self.itemRemoveOldEpisodes, \
168 self.item_report_bug, \
169 self.itemPreferences, \
170 self.item_support):
171 button = gtk.Button()
172 action.connect_proxy(button)
173 appmenu.append(button)
174 appmenu.show_all()
175 self.main_window.set_app_menu(appmenu)
177 # Initialize portrait mode / rotation manager
178 self._fremantle_rotation = FremantleRotation('gPodder', \
179 self.main_window, \
180 gpodder.__version__, \
181 self.config.rotation_mode)
183 self.bluetooth_available = False
184 else:
185 self.bluetooth_available = util.bluetooth_available()
186 self.toolbar.set_property('visible', self.config.show_toolbar)
188 self.config.connect_gtk_window(self.gPodder, 'main_window')
189 if not gpodder.ui.fremantle:
190 self.config.connect_gtk_paned('paned_position', self.channelPaned)
191 self.main_window.show()
193 self.gPodder.connect('key-press-event', self.on_key_press)
195 self.config.add_observer(self.on_config_changed)
197 self.tray_icon = None
198 self.episode_shownotes_window = None
199 self.new_episodes_window = None
201 if gpodder.ui.desktop:
202 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
203 self.main_window, self.show_confirmation, \
204 self.update_episode_list_icons, \
205 self.update_podcast_list_model, self.toolPreferences, \
206 gPodderEpisodeSelector)
207 else:
208 self.sync_ui = None
210 self.download_status_model = DownloadStatusModel()
211 self.download_queue_manager = download.DownloadQueueManager(self.config)
213 if gpodder.ui.desktop:
214 self.show_hide_tray_icon()
215 self.itemShowToolbar.set_active(self.config.show_toolbar)
216 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
218 if not gpodder.ui.fremantle:
219 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
220 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
221 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
222 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
224 # When the amount of maximum downloads changes, notify the queue manager
225 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
226 self.spinMaxDownloads.connect('value-changed', changed_cb)
228 self.default_title = 'gPodder'
229 if gpodder.__version__.rfind('git') != -1:
230 self.set_title('gPodder %s' % gpodder.__version__)
231 else:
232 title = self.gPodder.get_title()
233 if title is not None:
234 self.set_title(title)
235 else:
236 self.set_title(_('gPodder'))
238 self.cover_downloader = CoverDownloader()
240 # Generate list models for podcasts and their episodes
241 self.podcast_list_model = PodcastListModel(self.config.podcast_list_icon_size, self.cover_downloader)
243 self.cover_downloader.register('cover-available', self.cover_download_finished)
244 self.cover_downloader.register('cover-removed', self.cover_file_removed)
246 if gpodder.ui.fremantle:
247 self.button_subscribe.set_name('HildonButton-thumb')
248 self.button_podcasts.set_name('HildonButton-thumb')
249 self.button_downloads.set_name('HildonButton-thumb')
251 from gpodder.gtkui.frmntl import style
252 sub_font = style.get_font_desc('SmallSystemFont')
253 sub_color = style.get_color('SecondaryTextColor')
254 sub = (sub_font.to_string(), sub_color.to_string())
255 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
256 self.label_footer.set_markup(sub % gpodder.__copyright__)
258 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
259 while gtk.events_pending():
260 gtk.main_iteration(False)
262 try:
263 # Try to get the real package version from dpkg
264 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
265 version, _stderr = p.communicate()
266 del _stderr
267 del p
268 except:
269 version = gpodder.__version__
270 self.label_footer.set_markup(sub % ('v %s' % version))
272 self.episodes_window = gPodderEpisodes(self.main_window, \
273 on_treeview_expose_event=self.on_treeview_expose_event, \
274 show_episode_shownotes=self.show_episode_shownotes, \
275 update_podcast_list_model=self.update_podcast_list_model, \
276 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
277 item_view_episodes_all=self.item_view_episodes_all, \
278 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
279 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
280 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
281 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
282 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
283 hide_episode_search=self.hide_episode_search, \
284 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
285 playback_episodes=self.playback_episodes, \
286 delete_episode_list=self.delete_episode_list, \
287 episode_list_status_changed=self.episode_list_status_changed)
289 # Expose objects for episode list type-ahead find
290 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
291 self.entry_search_episodes = self.episodes_window.entry_search_episodes
292 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
294 def on_podcast_selected(channel):
295 self.active_channel = channel
296 self.update_episode_list_model()
297 self.episodes_window.channel = self.active_channel
298 self.episodes_window.show()
300 self.podcasts_window = gPodderPodcasts(self.main_window, \
301 show_podcast_episodes=on_podcast_selected, \
302 on_treeview_expose_event=self.on_treeview_expose_event, \
303 on_itemUpdate_activate=self.on_itemUpdate_activate, \
304 item_view_podcasts_all=self.item_view_podcasts_all, \
305 item_view_podcasts_downloaded=self.item_view_podcasts_downloaded, \
306 item_view_podcasts_unplayed=self.item_view_podcasts_unplayed, \
307 on_entry_search_podcasts_changed=self.on_entry_search_podcasts_changed, \
308 on_entry_search_podcasts_key_press=self.on_entry_search_podcasts_key_press, \
309 hide_podcast_search=self.hide_podcast_search, \
310 on_upload_to_mygpo=self.on_upload_to_mygpo, \
311 on_download_from_mygpo=self.on_download_from_mygpo, \
312 on_button_subscribe_clicked=self.on_button_subscribe_clicked)
314 # Expose objects for podcast list type-ahead find
315 self.hbox_search_podcasts = self.podcasts_window.hbox_search_podcasts
316 self.entry_search_podcasts = self.podcasts_window.entry_search_podcasts
317 self.button_search_podcasts_clear = self.podcasts_window.button_search_podcasts_clear
319 self.downloads_window = gPodderDownloads(self.main_window, \
320 on_treeview_expose_event=self.on_treeview_expose_event, \
321 on_btnCleanUpDownloads_clicked=self.on_btnCleanUpDownloads_clicked, \
322 _for_each_task_set_status=self._for_each_task_set_status, \
323 downloads_list_get_selection=self.downloads_list_get_selection)
324 self.treeChannels = self.podcasts_window.treeview
325 self.treeAvailable = self.episodes_window.treeview
326 self.treeDownloads = self.downloads_window.treeview
328 # Init the treeviews that we use
329 self.init_podcast_list_treeview()
330 self.init_episode_list_treeview()
331 self.init_download_list_treeview()
333 if self.config.podcast_list_hide_boring:
334 self.item_view_hide_boring_podcasts.set_active(True)
336 self.currently_updating = False
338 if gpodder.ui.maemo:
339 self.context_menu_mouse_button = 1
340 else:
341 self.context_menu_mouse_button = 3
343 if self.config.start_iconified:
344 self.iconify_main_window()
346 self.download_tasks_seen = set()
347 self.download_list_update_enabled = False
348 self.last_download_count = 0
350 # Subscribed channels
351 self.active_channel = None
352 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
353 self.channel_list_changed = True
354 self.update_podcasts_tab()
356 # load list of user applications for audio playback
357 self.user_apps_reader = UserAppsReader(['audio', 'video'])
358 def read_apps():
359 time.sleep(3) # give other parts of gpodder a chance to start up
360 self.user_apps_reader.read()
361 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
362 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
363 threading.Thread(target=read_apps).start()
365 # Set the "Device" menu item for the first time
366 if gpodder.ui.desktop:
367 self.update_item_device()
369 # Now, update the feed cache, when everything's in place
370 if not gpodder.ui.fremantle:
371 self.btnUpdateFeeds.show()
372 self.updating_feed_cache = False
373 self.feed_cache_update_cancelled = False
374 self.update_feed_cache(force_update=self.config.update_on_startup)
376 # Look for partial file downloads
377 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
379 # Message area
380 self.message_area = None
382 resumable_episodes = []
383 if len(partial_files) > 0:
384 for f in partial_files:
385 correct_name = f[:-len('.partial')] # strip ".partial"
386 log('Searching episode for file: %s', correct_name, sender=self)
387 found_episode = False
388 for c in self.channels:
389 for e in c.get_all_episodes():
390 if e.local_filename(create=False, check_only=True) == correct_name:
391 log('Found episode: %s', e.title, sender=self)
392 resumable_episodes.append(e)
393 found_episode = True
394 if found_episode:
395 break
396 if found_episode:
397 break
398 if not found_episode:
399 log('Partial file without episode: %s', f, sender=self)
400 util.delete_file(f)
402 if len(resumable_episodes):
403 self.download_episode_list_paused(resumable_episodes)
404 if not gpodder.ui.fremantle:
405 self.message_area = SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
406 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
407 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
408 self.message_area.show_all()
409 self.wNotebook.set_current_page(1)
411 self.clean_up_downloads(delete_partial=False)
412 else:
413 self.clean_up_downloads(delete_partial=True)
415 # Start the auto-update procedure
416 self._auto_update_timer_source_id = None
417 if self.config.auto_update_feeds:
418 self.restart_auto_update_timer()
420 # Delete old episodes if the user wishes to
421 if self.config.auto_remove_old_episodes:
422 old_episodes = self.get_old_episodes()
423 if len(old_episodes) > 0:
424 self.delete_episode_list(old_episodes, confirm=False)
425 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
427 if gpodder.ui.fremantle:
428 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
429 self.button_subscribe.set_sensitive(True)
430 self.button_podcasts.set_sensitive(True)
431 self.button_downloads.set_sensitive(True)
432 self.main_window.set_title(_('gPodder'))
434 # First-time users should be asked if they want to see the OPML
435 if not self.channels and not gpodder.ui.fremantle:
436 util.idle_add(self.on_itemUpdate_activate)
438 def on_button_subscribe_clicked(self, button):
439 self.on_itemImportChannels_activate(button)
441 def on_button_podcasts_clicked(self, widget):
442 if self.channels:
443 self.podcasts_window.show()
444 else:
445 gPodderWelcome(self.gPodder, \
446 show_example_podcasts_callback=self.on_itemImportChannels_activate, \
447 setup_my_gpodder_callback=self.on_download_from_mygpo)
449 def on_button_downloads_clicked(self, widget):
450 self.downloads_window.show()
452 def on_window_orientation_changed(self, orientation):
453 parent = self.vbox
454 old_container = parent.get_children()[0]
455 if orientation == Orientation.PORTRAIT:
456 container = gtk.VButtonBox()
457 else:
458 container = gtk.HButtonBox()
459 container.set_layout(old_container.get_layout())
460 for child in old_container.get_children():
461 if orientation == Orientation.LANDSCAPE:
462 child.set_alignment(0.5, 0.5, 0., 0.)
463 else:
464 child.set_alignment(0.5, 0.5, .9, 0.)
465 child.reparent(container)
466 container.show_all()
467 self.buttonbox = container
468 parent.remove(old_container)
469 parent.add(container)
470 parent.reorder_child(container, 0)
472 def on_treeview_podcasts_selection_changed(self, selection):
473 model, iter = selection.get_selected()
474 if iter is None:
475 self.active_channel = None
476 self.episode_list_model.clear()
478 def on_treeview_button_pressed(self, treeview, event):
479 if event.window != treeview.get_bin_window():
480 return False
482 TreeViewHelper.save_button_press_event(treeview, event)
484 if getattr(treeview, TreeViewHelper.ROLE) == \
485 TreeViewHelper.ROLE_PODCASTS:
486 return self.currently_updating
488 return event.button == self.context_menu_mouse_button and \
489 gpodder.ui.desktop
491 def on_treeview_podcasts_button_released(self, treeview, event):
492 if event.window != treeview.get_bin_window():
493 return False
495 if gpodder.ui.maemo:
496 return self.treeview_channels_handle_gestures(treeview, event)
497 return self.treeview_channels_show_context_menu(treeview, event)
499 def on_treeview_episodes_button_released(self, treeview, event):
500 if event.window != treeview.get_bin_window():
501 return False
503 if gpodder.ui.maemo:
504 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
505 return self.treeview_available_handle_gestures(treeview, event)
507 return self.treeview_available_show_context_menu(treeview, event)
509 def on_treeview_downloads_button_released(self, treeview, event):
510 if event.window != treeview.get_bin_window():
511 return False
513 return self.treeview_downloads_show_context_menu(treeview, event)
515 def on_entry_search_podcasts_changed(self, editable):
516 if self.hbox_search_podcasts.get_property('visible'):
517 self.podcast_list_model.set_search_term(editable.get_chars(0, -1))
519 def on_entry_search_podcasts_key_press(self, editable, event):
520 if event.keyval == gtk.keysyms.Escape:
521 self.hide_podcast_search()
522 return True
524 def hide_podcast_search(self, *args):
525 self.hbox_search_podcasts.hide()
526 self.entry_search_podcasts.set_text('')
527 self.podcast_list_model.set_search_term(None)
528 self.treeChannels.grab_focus()
530 def show_podcast_search(self, input_char):
531 self.hbox_search_podcasts.show()
532 self.entry_search_podcasts.insert_text(input_char, -1)
533 self.entry_search_podcasts.grab_focus()
534 self.entry_search_podcasts.set_position(-1)
536 def init_podcast_list_treeview(self):
537 # Set up podcast channel tree view widget
538 if gpodder.ui.fremantle:
539 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
540 self.item_view_podcasts_downloaded.set_active(True)
541 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
542 self.item_view_podcasts_unplayed.set_active(True)
543 else:
544 self.item_view_podcasts_all.set_active(True)
545 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
547 iconcolumn = gtk.TreeViewColumn('')
548 iconcell = gtk.CellRendererPixbuf()
549 iconcolumn.pack_start(iconcell, False)
550 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
551 self.treeChannels.append_column(iconcolumn)
553 namecolumn = gtk.TreeViewColumn('')
554 namecell = gtk.CellRendererText()
555 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
556 namecolumn.pack_start(namecell, True)
557 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
559 iconcell = gtk.CellRendererPixbuf()
560 iconcell.set_property('xalign', 1.0)
561 namecolumn.pack_start(iconcell, False)
562 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
563 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
564 self.treeChannels.append_column(namecolumn)
566 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
568 # When no podcast is selected, clear the episode list model
569 selection = self.treeChannels.get_selection()
570 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
572 # Set up type-ahead find for the podcast list
573 def on_key_press(treeview, event):
574 if event.keyval == gtk.keysyms.Escape:
575 self.hide_podcast_search()
576 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
577 self.hide_podcast_search()
578 elif event.state & gtk.gdk.CONTROL_MASK:
579 # Don't handle type-ahead when control is pressed (so shortcuts
580 # with the Ctrl key still work, e.g. Ctrl+A, ...)
581 return True
582 else:
583 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
584 if unicode_char_id == 0:
585 return False
586 input_char = unichr(unicode_char_id)
587 self.show_podcast_search(input_char)
588 return True
589 self.treeChannels.connect('key-press-event', on_key_press)
591 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
593 def on_entry_search_episodes_changed(self, editable):
594 if self.hbox_search_episodes.get_property('visible'):
595 self.episode_list_model.set_search_term(editable.get_chars(0, -1))
597 def on_entry_search_episodes_key_press(self, editable, event):
598 if event.keyval == gtk.keysyms.Escape:
599 self.hide_episode_search()
600 return True
602 def hide_episode_search(self, *args):
603 self.hbox_search_episodes.hide()
604 self.entry_search_episodes.set_text('')
605 self.episode_list_model.set_search_term(None)
606 self.treeAvailable.grab_focus()
608 def show_episode_search(self, input_char):
609 self.hbox_search_episodes.show()
610 self.entry_search_episodes.insert_text(input_char, -1)
611 self.entry_search_episodes.grab_focus()
612 self.entry_search_episodes.set_position(-1)
614 def init_episode_list_treeview(self):
615 self.episode_list_model = EpisodeListModel()
617 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
618 self.item_view_episodes_undeleted.set_active(True)
619 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
620 self.item_view_episodes_downloaded.set_active(True)
621 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
622 self.item_view_episodes_unplayed.set_active(True)
623 else:
624 self.item_view_episodes_all.set_active(True)
626 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
628 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
630 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
632 iconcell = gtk.CellRendererPixbuf()
633 if gpodder.ui.maemo:
634 iconcell.set_fixed_size(50, 50)
635 status_column_label = ''
636 else:
637 status_column_label = _('Status')
638 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
640 namecell = gtk.CellRendererText()
641 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
642 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
643 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
644 namecolumn.set_resizable(True)
645 namecolumn.set_expand(True)
647 sizecell = gtk.CellRendererText()
648 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
650 releasecell = gtk.CellRendererText()
651 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
653 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
654 itemcolumn.set_reorderable(True)
655 self.treeAvailable.append_column(itemcolumn)
657 if gpodder.ui.maemo:
658 sizecolumn.set_visible(False)
659 releasecolumn.set_visible(False)
661 # Set up type-ahead find for the episode list
662 def on_key_press(treeview, event):
663 if event.keyval == gtk.keysyms.Escape:
664 self.hide_episode_search()
665 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
666 self.hide_episode_search()
667 elif event.state & gtk.gdk.CONTROL_MASK:
668 # Don't handle type-ahead when control is pressed (so shortcuts
669 # with the Ctrl key still work, e.g. Ctrl+A, ...)
670 return False
671 else:
672 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
673 if unicode_char_id == 0:
674 return False
675 input_char = unichr(unicode_char_id)
676 self.show_episode_search(input_char)
677 return True
678 self.treeAvailable.connect('key-press-event', on_key_press)
680 if gpodder.ui.desktop:
681 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
682 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
683 def drag_data_get(tree, context, selection_data, info, timestamp):
684 if self.config.on_drag_mark_played:
685 for episode in self.get_selected_episodes():
686 episode.mark(is_played=True)
687 self.on_selected_episodes_status_changed()
688 uris = ['file://'+e.local_filename(create=False) \
689 for e in self.get_selected_episodes() \
690 if e.was_downloaded(and_exists=True)]
691 uris.append('') # for the trailing '\r\n'
692 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
693 self.treeAvailable.connect('drag-data-get', drag_data_get)
695 selection = self.treeAvailable.get_selection()
696 if gpodder.ui.diablo:
697 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
698 selection.set_mode(gtk.SELECTION_SINGLE)
699 else:
700 selection.set_mode(gtk.SELECTION_MULTIPLE)
701 elif gpodder.ui.fremantle:
702 selection.set_mode(gtk.SELECTION_SINGLE)
703 else:
704 selection.set_mode(gtk.SELECTION_MULTIPLE)
705 # Update the sensitivity of the toolbar buttons on the Desktop
706 selection.connect('changed', lambda s: self.play_or_download())
708 if gpodder.ui.diablo:
709 # Set up the tap-and-hold context menu for podcasts
710 menu = gtk.Menu()
711 menu.append(self.itemUpdateChannel.create_menu_item())
712 menu.append(self.itemEditChannel.create_menu_item())
713 menu.append(gtk.SeparatorMenuItem())
714 menu.append(self.itemRemoveChannel.create_menu_item())
715 menu.append(gtk.SeparatorMenuItem())
716 item = gtk.ImageMenuItem(_('Close this menu'))
717 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
718 gtk.ICON_SIZE_MENU))
719 menu.append(item)
720 menu.show_all()
721 menu = self.set_finger_friendly(menu)
722 self.treeChannels.tap_and_hold_setup(menu)
725 def init_download_list_treeview(self):
726 # enable multiple selection support
727 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
728 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
730 # columns and renderers for "download progress" tab
731 # First column: [ICON] Episodename
732 column = gtk.TreeViewColumn(_('Episode'))
734 cell = gtk.CellRendererPixbuf()
735 if gpodder.ui.maemo:
736 cell.set_fixed_size(50, 50)
737 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
738 column.pack_start(cell, expand=False)
739 column.add_attribute(cell, 'stock-id', \
740 DownloadStatusModel.C_ICON_NAME)
742 cell = gtk.CellRendererText()
743 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
744 column.pack_start(cell, expand=True)
745 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
746 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
747 column.set_expand(True)
748 self.treeDownloads.append_column(column)
750 # Second column: Progress
751 cell = gtk.CellRendererProgress()
752 cell.set_property('yalign', .5)
753 cell.set_property('ypad', 6)
754 column = gtk.TreeViewColumn(_('Progress'), cell,
755 value=DownloadStatusModel.C_PROGRESS, \
756 text=DownloadStatusModel.C_PROGRESS_TEXT)
757 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
758 column.set_expand(False)
759 column.set_property('min-width', 150)
760 column.set_property('max-width', 150)
761 self.treeDownloads.append_column(column)
763 self.treeDownloads.set_model(self.download_status_model)
764 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
766 def on_treeview_expose_event(self, treeview, event):
767 if event.window == treeview.get_bin_window():
768 model = treeview.get_model()
769 if (model is not None and model.get_iter_first() is not None):
770 return False
772 role = getattr(treeview, TreeViewHelper.ROLE)
773 ctx = event.window.cairo_create()
774 ctx.rectangle(event.area.x, event.area.y,
775 event.area.width, event.area.height)
776 ctx.clip()
778 x, y, width, height, depth = event.window.get_geometry()
780 if role == TreeViewHelper.ROLE_EPISODES:
781 if self.currently_updating:
782 text = _('Loading episodes') + '...'
783 elif self.config.episode_list_view_mode != \
784 EpisodeListModel.VIEW_ALL:
785 text = _('No episodes in current view')
786 else:
787 text = _('No episodes available')
788 elif role == TreeViewHelper.ROLE_PODCASTS:
789 if self.config.episode_list_view_mode != \
790 EpisodeListModel.VIEW_ALL and \
791 self.config.podcast_list_hide_boring and \
792 len(self.channels) > 0:
793 text = _('No podcasts in this view')
794 else:
795 text = _('No subscriptions')
796 elif role == TreeViewHelper.ROLE_DOWNLOADS:
797 text = _('No active downloads')
798 else:
799 raise Exception('on_treeview_expose_event: unknown role')
801 if gpodder.ui.fremantle:
802 from gpodder.gtkui.frmntl import style
803 font_desc = style.get_font_desc('LargeSystemFont')
804 else:
805 font_desc = None
807 draw_text_box_centered(ctx, treeview, width, height, text, font_desc)
809 return False
811 def enable_download_list_update(self):
812 if not self.download_list_update_enabled:
813 gobject.timeout_add(1500, self.update_downloads_list)
814 self.download_list_update_enabled = True
816 def on_btnCleanUpDownloads_clicked(self, button):
817 model = self.download_status_model
819 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
820 changed_episode_urls = []
821 for row_reference, task in all_tasks:
822 if task.status in (task.DONE, task.CANCELLED, task.FAILED):
823 model.remove(model.get_iter(row_reference.get_path()))
824 try:
825 # We don't "see" this task anymore - remove it;
826 # this is needed, so update_episode_list_icons()
827 # below gets the correct list of "seen" tasks
828 self.download_tasks_seen.remove(task)
829 except KeyError, key_error:
830 log('Cannot remove task from "seen" list: %s', task, sender=self)
831 changed_episode_urls.append(task.url)
832 # Tell the task that it has been removed (so it can clean up)
833 task.removed_from_list()
835 # Tell the podcasts tab to update icons for our removed podcasts
836 self.update_episode_list_icons(changed_episode_urls)
838 # Tell the shownotes window that we have removed the episode
839 if self.episode_shownotes_window is not None and \
840 self.episode_shownotes_window.episode is not None and \
841 self.episode_shownotes_window.episode.url in changed_episode_urls:
842 self.episode_shownotes_window._download_status_changed(None)
844 # Update the tab title and downloads list
845 self.update_downloads_list()
847 def on_tool_downloads_toggled(self, toolbutton):
848 if toolbutton.get_active():
849 self.wNotebook.set_current_page(1)
850 else:
851 self.wNotebook.set_current_page(0)
853 def update_downloads_list(self):
854 try:
855 model = self.download_status_model
857 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
858 total_speed, total_size, done_size = 0, 0, 0
860 # Keep a list of all download tasks that we've seen
861 download_tasks_seen = set()
863 # Remember the DownloadTask object for the episode that
864 # has been opened in the episode shownotes dialog (if any)
865 if self.episode_shownotes_window is not None:
866 shownotes_episode = self.episode_shownotes_window.episode
867 shownotes_task = None
868 else:
869 shownotes_episode = None
870 shownotes_task = None
872 # Do not go through the list of the model is not (yet) available
873 if model is None:
874 model = ()
876 for row in model:
877 self.download_status_model.request_update(row.iter)
879 task = row[self.download_status_model.C_TASK]
880 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
882 total_size += size
883 done_size += size*progress
885 if shownotes_episode is not None and \
886 shownotes_episode.url == task.episode.url:
887 shownotes_task = task
889 download_tasks_seen.add(task)
891 if status == download.DownloadTask.DOWNLOADING:
892 downloading += 1
893 total_speed += speed
894 elif status == download.DownloadTask.FAILED:
895 failed += 1
896 elif status == download.DownloadTask.DONE:
897 finished += 1
898 elif status == download.DownloadTask.QUEUED:
899 queued += 1
900 elif status == download.DownloadTask.PAUSED:
901 paused += 1
902 else:
903 others += 1
905 # Remember which tasks we have seen after this run
906 self.download_tasks_seen = download_tasks_seen
908 if gpodder.ui.desktop:
909 text = [_('Downloads')]
910 if downloading + failed + finished + queued > 0:
911 s = []
912 if downloading > 0:
913 s.append(_('%d active') % downloading)
914 if failed > 0:
915 s.append(_('%d failed') % failed)
916 if finished > 0:
917 s.append(_('%d done') % finished)
918 if queued > 0:
919 s.append(_('%d queued') % queued)
920 text.append(' (' + ', '.join(s)+')')
921 self.labelDownloads.set_text(''.join(text))
922 elif gpodder.ui.diablo:
923 sum = downloading + failed + finished + queued + paused + others
924 if sum:
925 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
926 else:
927 self.tool_downloads.set_label(_('Downloads'))
928 elif gpodder.ui.fremantle:
929 if downloading + queued > 0:
930 self.button_downloads.set_value(_('%d active') % (downloading+queued))
931 elif failed > 0:
932 self.button_downloads.set_value(_('%d failed') % failed)
933 elif paused > 0:
934 self.button_downloads.set_value(_('%d paused') % paused)
935 else:
936 self.button_downloads.set_value(_('None active'))
938 title = [self.default_title]
940 # We have to update all episodes/channels for which the status has
941 # changed. Accessing task.status_changed has the side effect of
942 # re-setting the changed flag, so we need to get the "changed" list
943 # of tuples first and split it into two lists afterwards
944 changed = [(task.url, task.podcast_url) for task in \
945 self.download_tasks_seen if task.status_changed]
946 episode_urls = [episode_url for episode_url, channel_url in changed]
947 channel_urls = [channel_url for episode_url, channel_url in changed]
949 count = downloading + queued
950 if count > 0:
951 if count == 1:
952 title.append( _('downloading one file'))
953 elif count > 1:
954 title.append( _('downloading %d files') % count)
956 if total_size > 0:
957 percentage = 100.0*done_size/total_size
958 else:
959 percentage = 0.0
960 total_speed = util.format_filesize(total_speed)
961 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
962 if self.tray_icon is not None:
963 # Update the tray icon status and progress bar
964 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
965 self.tray_icon.draw_progress_bar(percentage/100.)
966 elif self.last_download_count > 0:
967 if self.tray_icon is not None:
968 # Update the tray icon status
969 self.tray_icon.set_status()
970 self.tray_icon.downloads_finished(self.download_tasks_seen)
971 if gpodder.ui.diablo:
972 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
973 log('All downloads have finished.', sender=self)
974 if self.config.cmd_all_downloads_complete:
975 util.run_external_command(self.config.cmd_all_downloads_complete)
976 self.last_download_count = count
978 if not gpodder.ui.fremantle:
979 self.gPodder.set_title(' - '.join(title))
981 self.update_episode_list_icons(episode_urls)
982 if self.episode_shownotes_window is not None:
983 if (shownotes_task and shownotes_task.url in episode_urls) or \
984 shownotes_task != self.episode_shownotes_window.task:
985 self.episode_shownotes_window._download_status_changed(shownotes_task)
986 self.episode_shownotes_window._download_status_progress()
987 self.play_or_download()
988 if channel_urls:
989 self.update_podcast_list_model(channel_urls)
991 if not self.download_queue_manager.are_queued_or_active_tasks():
992 self.download_list_update_enabled = False
994 return self.download_list_update_enabled
995 except Exception, e:
996 log('Exception happened while updating download list.', sender=self, traceback=True)
997 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
998 # We return False here, so the update loop won't be called again,
999 # that's why we require the restart of gPodder in the message.
1000 return False
1002 def on_config_changed(self, name, old_value, new_value):
1003 if name == 'show_toolbar' and gpodder.ui.desktop:
1004 self.toolbar.set_property('visible', new_value)
1005 elif name == 'episode_list_descriptions':
1006 self.update_episode_list_model()
1007 elif name == 'rotation_mode':
1008 self._fremantle_rotation.set_mode(new_value)
1009 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1010 self.restart_auto_update_timer()
1012 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1013 # With get_bin_window, we get the window that contains the rows without
1014 # the header. The Y coordinate of this window will be the height of the
1015 # treeview header. This is the amount we have to subtract from the
1016 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1017 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1018 y -= x_bin
1019 y -= y_bin
1020 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1022 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
1023 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1024 return False
1026 if path is not None:
1027 model = treeview.get_model()
1028 iter = model.get_iter(path)
1029 role = getattr(treeview, TreeViewHelper.ROLE)
1031 if role == TreeViewHelper.ROLE_EPISODES:
1032 id = model.get_value(iter, EpisodeListModel.C_URL)
1033 elif role == TreeViewHelper.ROLE_PODCASTS:
1034 id = model.get_value(iter, PodcastListModel.C_URL)
1036 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1037 if last_tooltip is not None and last_tooltip != id:
1038 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1039 return False
1040 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1042 if role == TreeViewHelper.ROLE_EPISODES:
1043 description = model.get_value(iter, EpisodeListModel.C_DESCRIPTION_STRIPPED)
1044 if len(description) > 400:
1045 description = description[:398]+'[...]'
1047 tooltip.set_text(description)
1048 elif role == TreeViewHelper.ROLE_PODCASTS:
1049 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1050 channel.request_save_dir_size()
1051 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1052 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1053 if error_str:
1054 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1055 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1056 table = gtk.Table(rows=3, columns=3)
1057 table.set_row_spacings(5)
1058 table.set_col_spacings(5)
1059 table.set_border_width(5)
1061 heading = gtk.Label()
1062 heading.set_alignment(0, 1)
1063 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1064 table.attach(heading, 0, 1, 0, 1)
1065 size_info = gtk.Label()
1066 size_info.set_alignment(1, 1)
1067 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1068 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1069 table.attach(size_info, 2, 3, 0, 1)
1071 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1073 if len(channel.description) < 500:
1074 description = channel.description
1075 else:
1076 pos = channel.description.find('\n\n')
1077 if pos == -1 or pos > 500:
1078 description = channel.description[:498]+'[...]'
1079 else:
1080 description = channel.description[:pos]
1082 description = gtk.Label(description)
1083 if error_str:
1084 description.set_markup(error_str)
1085 description.set_alignment(0, 0)
1086 description.set_line_wrap(True)
1087 table.attach(description, 0, 3, 2, 3)
1089 table.show_all()
1090 tooltip.set_custom(table)
1092 return True
1094 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1095 return False
1097 def treeview_allow_tooltips(self, treeview, allow):
1098 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1100 def update_m3u_playlist_clicked(self, widget):
1101 if self.active_channel is not None:
1102 self.active_channel.update_m3u_playlist()
1103 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1105 def treeview_handle_context_menu_click(self, treeview, event):
1106 x, y = int(event.x), int(event.y)
1107 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1109 selection = treeview.get_selection()
1110 model, paths = selection.get_selected_rows()
1112 if path is None or (path not in paths and \
1113 event.button == self.context_menu_mouse_button):
1114 # We have right-clicked, but not into the selection,
1115 # assume we don't want to operate on the selection
1116 paths = []
1118 if path is not None and not paths and \
1119 event.button == self.context_menu_mouse_button:
1120 # No selection or clicked outside selection;
1121 # select the single item where we clicked
1122 treeview.grab_focus()
1123 treeview.set_cursor(path, column, 0)
1124 paths = [path]
1126 if not paths:
1127 # Unselect any remaining items (clicked elsewhere)
1128 if hasattr(treeview, 'is_rubber_banding_active'):
1129 if not treeview.is_rubber_banding_active():
1130 selection.unselect_all()
1131 else:
1132 selection.unselect_all()
1134 return model, paths
1136 def downloads_list_get_selection(self, model=None, paths=None):
1137 if model is None and paths is None:
1138 selection = self.treeDownloads.get_selection()
1139 model, paths = selection.get_selected_rows()
1141 can_queue, can_cancel, can_pause, can_remove = (True,)*4
1142 selected_tasks = [(gtk.TreeRowReference(model, path), \
1143 model.get_value(model.get_iter(path), \
1144 DownloadStatusModel.C_TASK)) for path in paths]
1146 for row_reference, task in selected_tasks:
1147 if task.status not in (download.DownloadTask.PAUSED, \
1148 download.DownloadTask.FAILED, \
1149 download.DownloadTask.CANCELLED):
1150 can_queue = False
1151 if task.status not in (download.DownloadTask.PAUSED, \
1152 download.DownloadTask.QUEUED, \
1153 download.DownloadTask.DOWNLOADING):
1154 can_cancel = False
1155 if task.status not in (download.DownloadTask.QUEUED, \
1156 download.DownloadTask.DOWNLOADING):
1157 can_pause = False
1158 if task.status not in (download.DownloadTask.CANCELLED, \
1159 download.DownloadTask.FAILED, \
1160 download.DownloadTask.DONE):
1161 can_remove = False
1163 return selected_tasks, can_queue, can_cancel, can_pause, can_remove
1165 def _for_each_task_set_status(self, tasks, status):
1166 episode_urls = set()
1167 model = self.treeDownloads.get_model()
1168 for row_reference, task in tasks:
1169 if status == download.DownloadTask.QUEUED:
1170 # Only queue task when its paused/failed/cancelled
1171 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1172 self.download_queue_manager.add_task(task)
1173 self.enable_download_list_update()
1174 elif status == download.DownloadTask.CANCELLED:
1175 # Cancelling a download allowed when downloading/queued
1176 if task.status in (task.QUEUED, task.DOWNLOADING):
1177 task.status = status
1178 # Cancelling paused downloads requires a call to .run()
1179 elif task.status == task.PAUSED:
1180 task.status = status
1181 # Call run, so the partial file gets deleted
1182 task.run()
1183 elif status == download.DownloadTask.PAUSED:
1184 # Pausing a download only when queued/downloading
1185 if task.status in (task.DOWNLOADING, task.QUEUED):
1186 task.status = status
1187 elif status is None:
1188 # Remove the selected task - cancel downloading/queued tasks
1189 if task.status in (task.QUEUED, task.DOWNLOADING):
1190 task.status = task.CANCELLED
1191 model.remove(model.get_iter(row_reference.get_path()))
1192 # Remember the URL, so we can tell the UI to update
1193 try:
1194 # We don't "see" this task anymore - remove it;
1195 # this is needed, so update_episode_list_icons()
1196 # below gets the correct list of "seen" tasks
1197 self.download_tasks_seen.remove(task)
1198 except KeyError, key_error:
1199 log('Cannot remove task from "seen" list: %s', task, sender=self)
1200 episode_urls.add(task.url)
1201 # Tell the task that it has been removed (so it can clean up)
1202 task.removed_from_list()
1203 else:
1204 # We can (hopefully) simply set the task status here
1205 task.status = status
1206 # Tell the podcasts tab to update icons for our removed podcasts
1207 self.update_episode_list_icons(episode_urls)
1208 # Update the tab title and downloads list
1209 self.update_downloads_list()
1211 def treeview_downloads_show_context_menu(self, treeview, event):
1212 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1213 if not paths:
1214 if not hasattr(treeview, 'is_rubber_banding_active'):
1215 return True
1216 else:
1217 return not treeview.is_rubber_banding_active()
1219 if event.button == self.context_menu_mouse_button:
1220 selected_tasks, can_queue, can_cancel, can_pause, can_remove = \
1221 self.downloads_list_get_selection(model, paths)
1223 def make_menu_item(label, stock_id, tasks, status, sensitive):
1224 # This creates a menu item for selection-wide actions
1225 item = gtk.ImageMenuItem(label)
1226 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1227 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status))
1228 item.set_sensitive(sensitive)
1229 return self.set_finger_friendly(item)
1231 menu = gtk.Menu()
1233 item = gtk.ImageMenuItem(_('Episode details'))
1234 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1235 if len(selected_tasks) == 1:
1236 row_reference, task = selected_tasks[0]
1237 episode = task.episode
1238 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1239 else:
1240 item.set_sensitive(False)
1241 menu.append(self.set_finger_friendly(item))
1242 menu.append(gtk.SeparatorMenuItem())
1243 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue))
1244 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1245 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1246 menu.append(gtk.SeparatorMenuItem())
1247 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1249 if gpodder.ui.maemo:
1250 # Because we open the popup on left-click for Maemo,
1251 # we also include a non-action to close the menu
1252 menu.append(gtk.SeparatorMenuItem())
1253 item = gtk.ImageMenuItem(_('Close this menu'))
1254 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1256 menu.append(self.set_finger_friendly(item))
1258 menu.show_all()
1259 menu.popup(None, None, None, event.button, event.time)
1260 return True
1262 def treeview_channels_show_context_menu(self, treeview, event):
1263 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1264 if not paths:
1265 return True
1267 if event.button == 3:
1268 menu = gtk.Menu()
1270 ICON = lambda x: x
1272 item = gtk.ImageMenuItem( _('Open download folder'))
1273 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1274 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1275 menu.append( item)
1277 item = gtk.ImageMenuItem( _('Update Feed'))
1278 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1279 item.connect('activate', self.on_itemUpdateChannel_activate )
1280 item.set_sensitive( not self.updating_feed_cache )
1281 menu.append( item)
1283 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1284 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1285 item.connect('activate', self.update_m3u_playlist_clicked)
1286 menu.append(item)
1288 if self.active_channel.link:
1289 item = gtk.ImageMenuItem(_('Visit website'))
1290 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1291 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1292 menu.append(item)
1294 if self.active_channel.channel_is_locked:
1295 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1296 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1297 item.connect('activate', self.on_channel_toggle_lock_activate)
1298 menu.append(self.set_finger_friendly(item))
1299 else:
1300 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1301 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1302 item.connect('activate', self.on_channel_toggle_lock_activate)
1303 menu.append(self.set_finger_friendly(item))
1306 menu.append( gtk.SeparatorMenuItem())
1308 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1309 item.connect( 'activate', self.on_itemEditChannel_activate)
1310 menu.append( item)
1312 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1313 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1314 menu.append( item)
1316 menu.show_all()
1317 # Disable tooltips while we are showing the menu, so
1318 # the tooltip will not appear over the menu
1319 self.treeview_allow_tooltips(self.treeChannels, False)
1320 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1321 menu.popup( None, None, None, event.button, event.time)
1323 return True
1325 def on_itemClose_activate(self, widget):
1326 if self.tray_icon is not None:
1327 self.iconify_main_window()
1328 else:
1329 self.on_gPodder_delete_event(widget)
1331 def cover_file_removed(self, channel_url):
1333 The Cover Downloader calls this when a previously-
1334 available cover has been removed from the disk. We
1335 have to update our model to reflect this change.
1337 self.podcast_list_model.delete_cover_by_url(channel_url)
1339 def cover_download_finished(self, channel_url, pixbuf):
1341 The Cover Downloader calls this when it has finished
1342 downloading (or registering, if already downloaded)
1343 a new channel cover, which is ready for displaying.
1345 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1347 def save_episode_as_file(self, episode):
1348 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1349 if episode.was_downloaded(and_exists=True):
1350 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1351 copy_from = episode.local_filename(create=False)
1352 assert copy_from is not None
1353 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1354 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1355 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1357 def copy_episodes_bluetooth(self, episodes):
1358 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1360 def convert_and_send_thread(episode):
1361 for episode in episodes:
1362 filename = episode.local_filename(create=False)
1363 assert filename is not None
1364 destfile = os.path.join(tempfile.gettempdir(), \
1365 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1366 (base, ext) = os.path.splitext(filename)
1367 if not destfile.endswith(ext):
1368 destfile += ext
1370 try:
1371 shutil.copyfile(filename, destfile)
1372 util.bluetooth_send_file(destfile)
1373 except:
1374 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1375 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1377 util.delete_file(destfile)
1379 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1381 def get_device_name(self):
1382 if self.config.device_type == 'ipod':
1383 return _('iPod')
1384 elif self.config.device_type in ('filesystem', 'mtp'):
1385 return _('MP3 player')
1386 else:
1387 return '(unknown device)'
1389 def _treeview_button_released(self, treeview, event):
1390 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1391 dy = int(abs(event.y-ypos))
1392 dx = int(event.x-xpos)
1394 selection = treeview.get_selection()
1395 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1396 if path is None or dy > 30:
1397 return (False, dx, dy)
1399 path, column, x, y = path
1400 selection.select_path(path)
1401 treeview.set_cursor(path)
1402 treeview.grab_focus()
1404 return (True, dx, dy)
1406 def treeview_channels_handle_gestures(self, treeview, event):
1407 if self.currently_updating:
1408 return False
1410 selected, dx, dy = self._treeview_button_released(treeview, event)
1412 if selected:
1413 if self.config.maemo_enable_gestures:
1414 if dx > 70:
1415 self.on_itemUpdateChannel_activate()
1416 elif dx < -70:
1417 self.on_itemEditChannel_activate(treeview)
1419 return False
1421 def treeview_available_handle_gestures(self, treeview, event):
1422 selected, dx, dy = self._treeview_button_released(treeview, event)
1424 if selected:
1425 if self.config.maemo_enable_gestures:
1426 if dx > 70:
1427 self.on_playback_selected_episodes(None)
1428 return True
1429 elif dx < -70:
1430 self.on_shownotes_selected_episodes(None)
1431 return True
1433 # Pass the event to the context menu handler for treeAvailable
1434 self.treeview_available_show_context_menu(treeview, event)
1436 return True
1438 def treeview_available_show_context_menu(self, treeview, event):
1439 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1440 if not paths:
1441 if not hasattr(treeview, 'is_rubber_banding_active'):
1442 return True
1443 else:
1444 return not treeview.is_rubber_banding_active()
1446 if event.button == self.context_menu_mouse_button:
1447 episodes = self.get_selected_episodes()
1448 any_locked = any(e.is_locked for e in episodes)
1449 any_played = any(e.is_played for e in episodes)
1450 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1452 menu = gtk.Menu()
1454 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1456 if open_instead_of_play:
1457 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1458 else:
1459 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1461 item.set_sensitive(can_play)
1462 item.connect('activate', self.on_playback_selected_episodes)
1463 menu.append(self.set_finger_friendly(item))
1465 if not can_cancel:
1466 item = gtk.ImageMenuItem(_('Download'))
1467 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1468 item.set_sensitive(can_download)
1469 item.connect('activate', self.on_download_selected_episodes)
1470 menu.append(self.set_finger_friendly(item))
1471 else:
1472 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1473 item.connect('activate', self.on_item_cancel_download_activate)
1474 menu.append(self.set_finger_friendly(item))
1476 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1477 item.set_sensitive(can_delete)
1478 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1479 menu.append(self.set_finger_friendly(item))
1481 if one_is_new:
1482 item = gtk.ImageMenuItem(_('Do not download'))
1483 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1484 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1485 menu.append(self.set_finger_friendly(item))
1486 elif can_download:
1487 item = gtk.ImageMenuItem(_('Mark as new'))
1488 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1489 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1490 menu.append(self.set_finger_friendly(item))
1492 ICON = lambda x: x
1494 # Ok, this probably makes sense to only display for downloaded files
1495 if can_play and not can_download:
1496 menu.append( gtk.SeparatorMenuItem())
1497 item = gtk.ImageMenuItem(_('Save to disk'))
1498 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1499 item.connect('activate', lambda w: [self.save_episode_as_file(e) for e in episodes])
1500 menu.append(self.set_finger_friendly(item))
1501 if self.bluetooth_available:
1502 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1503 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1504 item.connect('activate', lambda w: self.copy_episodes_bluetooth(episodes))
1505 menu.append(self.set_finger_friendly(item))
1506 if can_transfer:
1507 item = gtk.ImageMenuItem(_('Transfer to %s') % self.get_device_name())
1508 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1509 item.connect('activate', lambda w: self.on_sync_to_ipod_activate(w, episodes))
1510 menu.append(self.set_finger_friendly(item))
1512 if can_play:
1513 menu.append( gtk.SeparatorMenuItem())
1514 if any_played:
1515 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1516 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1517 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1518 menu.append(self.set_finger_friendly(item))
1519 else:
1520 item = gtk.ImageMenuItem(_('Mark as played'))
1521 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1522 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1523 menu.append(self.set_finger_friendly(item))
1525 if any_locked:
1526 item = gtk.ImageMenuItem(_('Allow deletion'))
1527 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1528 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, False))
1529 menu.append(self.set_finger_friendly(item))
1530 else:
1531 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1532 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1533 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, True))
1534 menu.append(self.set_finger_friendly(item))
1536 menu.append(gtk.SeparatorMenuItem())
1537 # Single item, add episode information menu item
1538 item = gtk.ImageMenuItem(_('Episode details'))
1539 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1540 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1541 menu.append(self.set_finger_friendly(item))
1543 # If we have it, also add episode website link
1544 if episodes[0].link and episodes[0].link != episodes[0].url:
1545 item = gtk.ImageMenuItem(_('Visit website'))
1546 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1547 item.connect('activate', lambda w: util.open_website(episodes[0].link))
1548 menu.append(self.set_finger_friendly(item))
1550 if gpodder.ui.maemo:
1551 # Because we open the popup on left-click for Maemo,
1552 # we also include a non-action to close the menu
1553 menu.append(gtk.SeparatorMenuItem())
1554 item = gtk.ImageMenuItem(_('Close this menu'))
1555 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1556 menu.append(self.set_finger_friendly(item))
1558 menu.show_all()
1559 # Disable tooltips while we are showing the menu, so
1560 # the tooltip will not appear over the menu
1561 self.treeview_allow_tooltips(self.treeAvailable, False)
1562 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1563 menu.popup( None, None, None, event.button, event.time)
1565 return True
1567 def set_title(self, new_title):
1568 if not gpodder.ui.fremantle:
1569 self.default_title = new_title
1570 self.gPodder.set_title(new_title)
1572 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1574 Updates the status icons in the episode list.
1576 If urls is given, it should be a list of URLs
1577 of episodes that should be updated.
1579 If urls is None, set ONE OF selected, all to
1580 True (the former updates just the selected
1581 episodes and the latter updates all episodes).
1583 if urls is not None:
1584 # We have a list of URLs to walk through
1585 self.episode_list_model.update_by_urls(urls, \
1586 self.episode_is_downloading, \
1587 self.config.episode_list_descriptions and \
1588 gpodder.ui.desktop)
1589 elif selected and not all:
1590 # We should update all selected episodes
1591 selection = self.treeAvailable.get_selection()
1592 model, paths = selection.get_selected_rows()
1593 for path in reversed(paths):
1594 iter = model.get_iter(path)
1595 self.episode_list_model.update_by_filter_iter(iter, \
1596 self.episode_is_downloading, \
1597 self.config.episode_list_descriptions and \
1598 gpodder.ui.desktop)
1599 elif all and not selected:
1600 # We update all (even the filter-hidden) episodes
1601 self.episode_list_model.update_all(\
1602 self.episode_is_downloading, \
1603 self.config.episode_list_descriptions and \
1604 gpodder.ui.desktop)
1605 else:
1606 # Wrong/invalid call - have to specify at least one parameter
1607 raise ValueError('Invalid call to update_episode_list_icons')
1609 def episode_list_status_changed(self, episodes):
1610 self.update_episode_list_icons(set(e.url for e in episodes))
1611 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1612 self.db.commit()
1614 def clean_up_downloads(self, delete_partial=False):
1615 # Clean up temporary files left behind by old gPodder versions
1616 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1618 if delete_partial:
1619 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1621 for tempfile in temporary_files:
1622 util.delete_file(tempfile)
1624 # Clean up empty download folders and abandoned download folders
1625 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1626 for ddir in download_dirs:
1627 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1628 globr = glob.glob(os.path.join(ddir, '*'))
1629 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1630 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
1631 shutil.rmtree(ddir, ignore_errors=True)
1633 def streaming_possible(self):
1634 if gpodder.ui.desktop:
1635 # User has to have a media player set on the Desktop, or else we
1636 # would probably open the browser when giving a URL to xdg-open..
1637 return (self.config.player and self.config.player != 'default')
1638 elif gpodder.ui.maemo:
1639 # On Maemo, the default is to use the Nokia Media Player, which is
1640 # already able to deal with HTTP URLs the right way, so we
1641 # unconditionally enable streaming always on Maemo
1642 return True
1644 return False
1646 def playback_episodes_for_real(self, episodes):
1647 groups = collections.defaultdict(list)
1648 for episode in episodes:
1649 file_type = episode.file_type()
1650 if file_type == 'video' and self.config.videoplayer and \
1651 self.config.videoplayer != 'default':
1652 player = self.config.videoplayer
1653 if gpodder.ui.diablo:
1654 # Use the wrapper script if it's installed to crop 3GP YouTube
1655 # videos to fit the screen (looks much nicer than w/ black border)
1656 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
1657 player = 'gpodder-mplayer'
1658 elif file_type == 'audio' and self.config.player and \
1659 self.config.player != 'default':
1660 player = self.config.player
1661 else:
1662 player = 'default'
1664 if file_type not in ('audio', 'video') or \
1665 (file_type == 'audio' and not self.config.audio_played_dbus) or \
1666 (file_type == 'video' and not self.config.video_played_dbus):
1667 # Mark episode as played in the database
1668 episode.mark(is_played=True)
1670 filename = episode.local_filename(create=False)
1671 if filename is None or not os.path.exists(filename):
1672 filename = episode.url
1673 groups[player].append(filename)
1675 # Open episodes with system default player
1676 if 'default' in groups:
1677 for filename in groups['default']:
1678 log('Opening with system default: %s', filename, sender=self)
1679 util.gui_open(filename)
1680 del groups['default']
1681 elif gpodder.ui.maemo:
1682 # When on Maemo and not opening with default, show a notification
1683 # (no startup notification for Panucci / MPlayer yet...)
1684 if len(episodes) == 1:
1685 text = _('Opening %s') % episodes[0].title
1686 else:
1687 text = _('Opening %d episodes') % len(episodes)
1689 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
1691 def destroy_banner_later(banner):
1692 banner.destroy()
1693 return False
1694 gobject.timeout_add(5000, destroy_banner_later, banner)
1696 # For each type now, go and create play commands
1697 for group in groups:
1698 for command in util.format_desktop_command(group, groups[group]):
1699 log('Executing: %s', repr(command), sender=self)
1700 subprocess.Popen(command)
1702 def playback_episodes(self, episodes):
1703 episodes = [e for e in episodes if \
1704 e.was_downloaded(and_exists=True) or self.streaming_possible()]
1706 try:
1707 self.playback_episodes_for_real(episodes)
1708 except Exception, e:
1709 log('Error in playback!', sender=self, traceback=True)
1710 if gpodder.ui.desktop:
1711 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
1712 _('Error opening player'), widget=self.toolPreferences)
1713 else:
1714 self.show_message(_('Please check your media player settings in the preferences dialog.'))
1716 channel_urls = set()
1717 episode_urls = set()
1718 for episode in episodes:
1719 channel_urls.add(episode.channel.url)
1720 episode_urls.add(episode.url)
1721 self.update_episode_list_icons(episode_urls)
1722 self.update_podcast_list_model(channel_urls)
1724 def play_or_download(self):
1725 if not gpodder.ui.fremantle:
1726 if self.wNotebook.get_current_page() > 0:
1727 if gpodder.ui.desktop:
1728 self.toolCancel.set_sensitive(True)
1729 return
1731 if self.currently_updating:
1732 return (False, False, False, False, False, False)
1734 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1735 ( is_played, is_locked ) = (False,)*2
1737 open_instead_of_play = False
1739 selection = self.treeAvailable.get_selection()
1740 if selection.count_selected_rows() > 0:
1741 (model, paths) = selection.get_selected_rows()
1743 for path in paths:
1744 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
1746 if episode.file_type() not in ('audio', 'video'):
1747 open_instead_of_play = True
1749 if episode.was_downloaded():
1750 can_play = episode.was_downloaded(and_exists=True)
1751 can_delete = True
1752 is_played = episode.is_played
1753 is_locked = episode.is_locked
1754 if not can_play:
1755 can_download = True
1756 else:
1757 if self.episode_is_downloading(episode):
1758 can_cancel = True
1759 else:
1760 can_download = True
1762 can_download = can_download and not can_cancel
1763 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
1764 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
1766 if gpodder.ui.desktop:
1767 if open_instead_of_play:
1768 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1769 else:
1770 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1771 self.toolPlay.set_sensitive( can_play)
1772 self.toolDownload.set_sensitive( can_download)
1773 self.toolTransfer.set_sensitive( can_transfer)
1774 self.toolCancel.set_sensitive( can_cancel)
1776 if not gpodder.ui.fremantle:
1777 self.item_cancel_download.set_sensitive(can_cancel)
1778 self.itemDownloadSelected.set_sensitive(can_download)
1779 self.itemOpenSelected.set_sensitive(can_play)
1780 self.itemPlaySelected.set_sensitive(can_play)
1781 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1782 self.item_toggle_played.set_sensitive(can_play)
1783 self.item_toggle_lock.set_sensitive(can_play)
1784 self.itemOpenSelected.set_visible(open_instead_of_play)
1785 self.itemPlaySelected.set_visible(not open_instead_of_play)
1787 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1789 def on_cbMaxDownloads_toggled(self, widget, *args):
1790 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1792 def on_cbLimitDownloads_toggled(self, widget, *args):
1793 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1795 def episode_new_status_changed(self, urls):
1796 self.update_podcast_list_model()
1797 self.update_episode_list_icons(urls)
1799 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
1800 """Update the podcast list treeview model
1802 If urls is given, it should list the URLs of each
1803 podcast that has to be updated in the list.
1805 If selected is True, only update the model contents
1806 for the currently-selected podcast - nothing more.
1808 The caller can optionally specify "select_url",
1809 which is the URL of the podcast that is to be
1810 selected in the list after the update is complete.
1811 This only works if the podcast list has to be
1812 reloaded; i.e. something has been added or removed
1813 since the last update of the podcast list).
1815 selection = self.treeChannels.get_selection()
1816 model, iter = selection.get_selected()
1818 if selected:
1819 # very cheap! only update selected channel
1820 if iter is not None:
1821 self.podcast_list_model.update_by_filter_iter(iter)
1822 elif not self.channel_list_changed:
1823 # we can keep the model, but have to update some
1824 if urls is None:
1825 # still cheaper than reloading the whole list
1826 self.podcast_list_model.update_all()
1827 else:
1828 # ok, we got a bunch of urls to update
1829 self.podcast_list_model.update_by_urls(urls)
1830 else:
1831 if model and iter and select_url is None:
1832 # Get the URL of the currently-selected podcast
1833 select_url = model.get_value(iter, PodcastListModel.C_URL)
1835 # Update the podcast list model with new channels
1836 self.podcast_list_model.set_channels(self.channels)
1838 try:
1839 selected_iter = model.get_iter_first()
1840 # Find the previously-selected URL in the new
1841 # model if we have an URL (else select first)
1842 if select_url is not None:
1843 pos = model.get_iter_first()
1844 while pos is not None:
1845 url = model.get_value(pos, PodcastListModel.C_URL)
1846 if url == select_url:
1847 selected_iter = pos
1848 break
1849 pos = model.iter_next(pos)
1851 if not gpodder.ui.fremantle:
1852 if selected_iter is not None:
1853 selection.select_iter(selected_iter)
1854 self.on_treeChannels_cursor_changed(self.treeChannels)
1855 except:
1856 log('Cannot select podcast in list', traceback=True, sender=self)
1857 self.channel_list_changed = False
1859 def episode_is_downloading(self, episode):
1860 """Returns True if the given episode is being downloaded at the moment"""
1861 if episode is None:
1862 return False
1864 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1866 def update_episode_list_model(self):
1867 if self.channels and self.active_channel is not None:
1868 if gpodder.ui.diablo:
1869 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes'))
1870 else:
1871 banner = None
1873 if gpodder.ui.fremantle:
1874 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
1876 self.currently_updating = True
1877 self.episode_list_model.clear()
1878 def do_update_episode_list_model():
1879 self.episode_list_model.add_from_channel(\
1880 self.active_channel, \
1881 self.episode_is_downloading, \
1882 self.config.episode_list_descriptions \
1883 and gpodder.ui.desktop)
1885 def on_episode_list_model_updated():
1886 if banner is not None:
1887 banner.destroy()
1888 if gpodder.ui.fremantle:
1889 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
1890 self.treeAvailable.columns_autosize()
1891 self.currently_updating = False
1892 self.play_or_download()
1893 util.idle_add(on_episode_list_model_updated)
1894 threading.Thread(target=do_update_episode_list_model).start()
1895 else:
1896 self.episode_list_model.clear()
1898 def offer_new_episodes(self, channels=None):
1899 new_episodes = self.get_new_episodes(channels)
1900 if new_episodes:
1901 self.new_episodes_show(new_episodes)
1902 return True
1903 return False
1905 def add_podcast_list(self, urls, auth_tokens=None):
1906 """Subscribe to a list of podcast given their URLs
1908 If auth_tokens is given, it should be a dictionary
1909 mapping URLs to (username, password) tuples."""
1911 if auth_tokens is None:
1912 auth_tokens = {}
1914 # Sort and split the URL list into five buckets
1915 queued, failed, existing, worked, authreq = [], [], [], [], []
1916 for input_url in urls:
1917 url = util.normalize_feed_url(input_url)
1918 if url is None:
1919 # Fail this one because the URL is not valid
1920 failed.append(input_url)
1921 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
1922 # A podcast already exists in the list for this URL
1923 existing.append(url)
1924 else:
1925 # This URL has survived the first round - queue for add
1926 queued.append(url)
1927 if url != input_url and input_url in auth_tokens:
1928 auth_tokens[url] = auth_tokens[input_url]
1930 error_messages = {}
1931 redirections = {}
1933 progress = ProgressIndicator(_('Adding podcasts'), \
1934 _('Please wait while episode information is downloaded.'), \
1935 parent=self.main_window)
1937 def on_after_update():
1938 progress.on_finished()
1939 # Report already-existing subscriptions to the user
1940 if existing:
1941 title = _('Existing subscriptions skipped')
1942 message = _('You are already subscribed to these podcasts:') \
1943 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
1944 self.show_message(message, title, widget=self.treeChannels)
1946 # Report subscriptions that require authentication
1947 if authreq:
1948 retry_podcasts = {}
1949 for url in authreq:
1950 title = _('Podcast requires authentication')
1951 message = _('Please login to %s:') % (saxutils.escape(url),)
1952 success, auth_tokens = self.show_login_dialog(title, message)
1953 if success:
1954 retry_podcasts[url] = auth_tokens
1955 else:
1956 # Stop asking the user for more login data
1957 retry_podcasts = {}
1958 for url in authreq:
1959 error_messages[url] = _('Authentication failed')
1960 failed.append(url)
1961 break
1963 # If we have authentication data to retry, do so here
1964 if retry_podcasts:
1965 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
1967 # Report website redirections
1968 for url in redirections:
1969 title = _('Website redirection detected')
1970 message = _('The URL %s redirects to %s.') \
1971 + '\n\n' + _('Do you want to visit the website now?')
1972 message = message % (url, redirections[url])
1973 if self.show_confirmation(message, title):
1974 util.open_website(url)
1975 else:
1976 break
1978 # Report failed subscriptions to the user
1979 if failed:
1980 title = _('Could not add some podcasts')
1981 message = _('Some podcasts could not be added to your list:') \
1982 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
1983 error_messages.get(url, _('Unknown')))) for url in failed)
1984 self.show_message(message, title, important=True)
1986 # If at least one podcast has been added, save and update all
1987 if self.channel_list_changed:
1988 self.save_channels_opml()
1990 # If only one podcast was added, select it after the update
1991 if len(worked) == 1:
1992 url = worked[0]
1993 else:
1994 url = None
1996 # Update the list of subscribed podcasts
1997 self.update_feed_cache(force_update=False, select_url_afterwards=url)
1998 self.update_podcasts_tab()
2000 # Offer to download new episodes
2001 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
2003 def thread_proc():
2004 # After the initial sorting and splitting, try all queued podcasts
2005 length = len(queued)
2006 for index, url in enumerate(queued):
2007 progress.on_progress(float(index)/float(length))
2008 progress.on_message(url)
2009 log('QUEUE RUNNER: %s', url, sender=self)
2010 try:
2011 # The URL is valid and does not exist already - subscribe!
2012 channel = PodcastChannel.load(self.db, url=url, create=True, \
2013 authentication_tokens=auth_tokens.get(url, None), \
2014 max_episodes=self.config.max_episodes_per_feed, \
2015 download_dir=self.config.download_dir, \
2016 allow_empty_feeds=self.config.allow_empty_feeds)
2018 try:
2019 username, password = util.username_password_from_url(url)
2020 except ValueError, ve:
2021 username, password = (None, None)
2023 if username is not None and channel.username is None and \
2024 password is not None and channel.password is None:
2025 channel.username = username
2026 channel.password = password
2027 channel.save()
2029 self._update_cover(channel)
2030 except feedcore.AuthenticationRequired:
2031 if url in auth_tokens:
2032 # Fail for wrong authentication data
2033 error_messages[url] = _('Authentication failed')
2034 failed.append(url)
2035 else:
2036 # Queue for login dialog later
2037 authreq.append(url)
2038 continue
2039 except feedcore.WifiLogin, error:
2040 redirections[url] = error.data
2041 failed.append(url)
2042 error_messages[url] = _('Redirection detected')
2043 continue
2044 except Exception, e:
2045 log('Subscription error: %s', e, traceback=True, sender=self)
2046 error_messages[url] = str(e)
2047 failed.append(url)
2048 continue
2050 assert channel is not None
2051 worked.append(channel.url)
2052 self.channels.append(channel)
2053 self.channel_list_changed = True
2054 util.idle_add(on_after_update)
2055 threading.Thread(target=thread_proc).start()
2057 def save_channels_opml(self):
2058 exporter = opml.Exporter(gpodder.subscription_file)
2059 return exporter.write(self.channels)
2061 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2062 self.db.commit()
2063 self.updating_feed_cache = False
2065 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2066 self.channel_list_changed = True
2067 self.update_podcast_list_model(select_url=select_url_afterwards)
2069 # Only search for new episodes in podcasts that have been
2070 # updated, not in other podcasts (for single-feed updates)
2071 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2073 if gpodder.ui.fremantle:
2074 self.button_subscribe.set_sensitive(True)
2075 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2076 self.update_podcasts_tab()
2077 if episodes:
2078 if self.config.auto_download == 'always':
2079 if len(episodes) == 1:
2080 title = _('Downloading one new episode.')
2081 else:
2082 title = _('Downloading %d new episodes.') % len(episodes)
2083 self.show_message(title)
2084 self.download_episode_list(episodes)
2085 elif self.config.auto_download == 'queue':
2086 self.show_message(_('New episodes have been added to the download list.'))
2087 self.download_episode_list_paused(episodes)
2088 else:
2089 self.new_episodes_show(episodes)
2090 elif not self.config.auto_update_feeds:
2091 self.show_message(_('No new episodes. Please check for new episodes later.'))
2092 return
2094 if self.tray_icon:
2095 self.tray_icon.set_status()
2097 if self.feed_cache_update_cancelled:
2098 # The user decided to abort the feed update
2099 self.show_update_feeds_buttons()
2100 elif not episodes:
2101 # Nothing new here - but inform the user
2102 self.pbFeedUpdate.set_fraction(1.0)
2103 self.pbFeedUpdate.set_text(_('No new episodes'))
2104 self.feed_cache_update_cancelled = True
2105 self.btnCancelFeedUpdate.show()
2106 self.btnCancelFeedUpdate.set_sensitive(True)
2107 if gpodder.ui.maemo:
2108 # btnCancelFeedUpdate is a ToolButton on Maemo
2109 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2110 else:
2111 # btnCancelFeedUpdate is a normal gtk.Button
2112 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2113 else:
2114 # New episodes are available
2115 self.pbFeedUpdate.set_fraction(1.0)
2116 # Are we minimized and should we auto download?
2117 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2118 self.download_episode_list(episodes)
2119 if len(episodes) == 1:
2120 title = _('Downloading one new episode.')
2121 else:
2122 title = _('Downloading %d new episodes.') % len(episodes)
2124 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2125 self.show_update_feeds_buttons()
2126 else:
2127 self.show_update_feeds_buttons()
2128 # New episodes are available and we are not minimized
2129 if not self.config.do_not_show_new_episodes_dialog:
2130 self.new_episodes_show(episodes, notification=True)
2131 else:
2132 if len(episodes) == 1:
2133 message = _('One new episode is available for download')
2134 else:
2135 message = _('%i new episodes are available for download' % len(episodes))
2137 self.pbFeedUpdate.set_text(message)
2139 def _update_cover(self, channel):
2140 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2141 self.cover_downloader.request_cover(channel)
2143 def update_feed_cache_proc(self, channels, select_url_afterwards):
2144 total = len(channels)
2146 for updated, channel in enumerate(channels):
2147 if not self.feed_cache_update_cancelled:
2148 try:
2149 # Update if timeout is not reached or we update a single podcast or skipping is disabled
2150 if channel.query_automatic_update() or total == 1 or not self.config.feed_update_skipping:
2151 channel.update(max_episodes=self.config.max_episodes_per_feed)
2152 else:
2153 log('Skipping update of %s (see feed_update_skipping)', channel.title, sender=self)
2154 self._update_cover(channel)
2155 except Exception, e:
2156 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)
2157 log('Error: %s', str(e), sender=self, traceback=True)
2159 if self.feed_cache_update_cancelled:
2160 break
2162 if gpodder.ui.fremantle:
2163 self.button_podcasts.set_value(_('%d/%d updated') % (updated, total))
2164 continue
2166 # By the time we get here the update may have already been cancelled
2167 if not self.feed_cache_update_cancelled:
2168 def update_progress():
2169 progression = _('Updated %s (%d/%d)') % (channel.title, updated, total)
2170 self.pbFeedUpdate.set_text(progression)
2171 if self.tray_icon:
2172 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2173 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
2174 util.idle_add(update_progress)
2176 updated_urls = [c.url for c in channels]
2177 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2179 def show_update_feeds_buttons(self):
2180 # Make sure that the buttons for updating feeds
2181 # appear - this should happen after a feed update
2182 if gpodder.ui.maemo:
2183 self.btnUpdateSelectedFeed.show()
2184 self.toolFeedUpdateProgress.hide()
2185 self.btnCancelFeedUpdate.hide()
2186 self.btnCancelFeedUpdate.set_is_important(False)
2187 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2188 self.toolbarSpacer.set_expand(True)
2189 self.toolbarSpacer.set_draw(False)
2190 else:
2191 self.hboxUpdateFeeds.hide()
2192 self.btnUpdateFeeds.show()
2193 self.itemUpdate.set_sensitive(True)
2194 self.itemUpdateChannel.set_sensitive(True)
2196 def on_btnCancelFeedUpdate_clicked(self, widget):
2197 if not self.feed_cache_update_cancelled:
2198 self.pbFeedUpdate.set_text(_('Cancelling...'))
2199 self.feed_cache_update_cancelled = True
2200 self.btnCancelFeedUpdate.set_sensitive(False)
2201 else:
2202 self.show_update_feeds_buttons()
2204 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2205 if self.updating_feed_cache:
2206 return
2208 if not force_update:
2209 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2210 self.channel_list_changed = True
2211 self.update_podcast_list_model(select_url=select_url_afterwards)
2212 return
2214 self.updating_feed_cache = True
2216 if channels is None:
2217 channels = self.channels
2219 if gpodder.ui.fremantle:
2220 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
2221 hildon.hildon_banner_show_information(self.main_window, \
2222 '', _('Updating podcast feeds'))
2223 self.button_podcasts.set_value(_('Updating...'))
2224 self.button_subscribe.set_sensitive(False)
2225 else:
2226 self.itemUpdate.set_sensitive(False)
2227 self.itemUpdateChannel.set_sensitive(False)
2229 if self.tray_icon:
2230 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2232 if len(channels) == 1:
2233 text = _('Updating "%s"...') % channels[0].title
2234 else:
2235 text = _('Updating %d feeds...') % len(channels)
2236 self.pbFeedUpdate.set_text(text)
2237 self.pbFeedUpdate.set_fraction(0)
2239 self.feed_cache_update_cancelled = False
2240 self.btnCancelFeedUpdate.show()
2241 self.btnCancelFeedUpdate.set_sensitive(True)
2242 if gpodder.ui.maemo:
2243 self.toolbarSpacer.set_expand(False)
2244 self.toolbarSpacer.set_draw(True)
2245 self.btnUpdateSelectedFeed.hide()
2246 self.toolFeedUpdateProgress.show_all()
2247 else:
2248 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2249 self.hboxUpdateFeeds.show_all()
2250 self.btnUpdateFeeds.hide()
2252 args = (channels, select_url_afterwards)
2253 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2255 def on_gPodder_delete_event(self, widget, *args):
2256 """Called when the GUI wants to close the window
2257 Displays a confirmation dialog (and closes/hides gPodder)
2260 downloading = self.download_status_model.are_downloads_in_progress()
2262 # Only iconify if we are using the window's "X" button,
2263 # but not when we are using "Quit" in the menu or toolbar
2264 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2265 self.iconify_main_window()
2266 elif self.config.on_quit_ask or downloading:
2267 if gpodder.ui.fremantle:
2268 self.close_gpodder()
2269 elif gpodder.ui.diablo:
2270 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2271 if result:
2272 self.close_gpodder()
2273 else:
2274 return True
2275 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2276 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2277 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2279 title = _('Quit gPodder')
2280 if downloading:
2281 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2282 else:
2283 message = _('Do you really want to quit gPodder now?')
2285 dialog.set_title(title)
2286 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2287 if not downloading:
2288 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2289 dialog.vbox.pack_start(cb_ask)
2290 cb_ask.show_all()
2292 quit_button.grab_focus()
2293 result = dialog.run()
2294 dialog.destroy()
2296 if result == gtk.RESPONSE_CLOSE:
2297 if not downloading and cb_ask.get_active() == True:
2298 self.config.on_quit_ask = False
2299 self.close_gpodder()
2300 else:
2301 self.close_gpodder()
2303 return True
2305 def close_gpodder(self):
2306 """ clean everything and exit properly
2308 if self.channels:
2309 if self.save_channels_opml():
2310 if self.config.my_gpodder_autoupload:
2311 log('Uploading to my.gpodder.org on close', sender=self)
2312 util.idle_add(self.on_upload_to_mygpo, None)
2313 else:
2314 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2316 self.gPodder.hide()
2318 if self.tray_icon is not None:
2319 self.tray_icon.set_visible(False)
2321 # Notify all tasks to to carry out any clean-up actions
2322 self.download_status_model.tell_all_tasks_to_quit()
2324 while gtk.events_pending():
2325 gtk.main_iteration(False)
2327 self.db.close()
2329 self.quit()
2330 sys.exit(0)
2332 def get_old_episodes(self):
2333 episodes = []
2334 for channel in self.channels:
2335 for episode in channel.get_downloaded_episodes():
2336 if episode.age_in_days() > self.config.episode_old_age and \
2337 not episode.is_locked and episode.is_played:
2338 episodes.append(episode)
2339 return episodes
2341 def delete_episode_list(self, episodes, confirm=True):
2342 if not episodes:
2343 return False
2345 count = len(episodes)
2347 if count == 1:
2348 episode = episodes[0]
2349 if episode.is_locked:
2350 title = _('%s is locked') % saxutils.escape(episode.title)
2351 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2352 self.notification(message, title, widget=self.treeAvailable)
2353 return False
2355 title = _('Remove %s?') % saxutils.escape(episode.title)
2356 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.")
2357 else:
2358 title = _('Remove %d episodes?') % count
2359 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.')
2361 locked_count = sum(int(e.is_locked) for e in episodes if e.is_locked is not None)
2363 if count == locked_count:
2364 title = _('Episodes are locked')
2365 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2366 self.notification(message, title, widget=self.treeAvailable)
2367 return False
2368 elif locked_count > 0:
2369 title = _('Remove %d out of %d episodes?') % (count-locked_count, count)
2370 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.')
2372 if confirm and not self.show_confirmation(message, title):
2373 return False
2375 progress = ProgressIndicator(_('Removing episodes'), \
2376 _('Please wait while episodes are deleted'), \
2377 parent=self.main_window)
2379 def finish_deletion(episode_urls, channel_urls):
2380 progress.on_finished()
2382 # Episodes have been deleted - persist the database
2383 self.db.commit()
2385 self.update_episode_list_icons(episode_urls)
2386 self.update_podcast_list_model(channel_urls)
2387 self.play_or_download()
2389 def thread_proc():
2390 episode_urls = set()
2391 channel_urls = set()
2393 for idx, episode in enumerate(episodes):
2394 progress.on_progress(float(idx)/float(len(episodes)))
2395 if episode.is_locked:
2396 log('Not deleting episode (is locked): %s', episode.title)
2397 else:
2398 log('Deleting episode: %s', episode.title)
2399 progress.on_message(_('Deleting: %s') % episode.title)
2400 episode.delete_from_disk()
2401 episode_urls.add(episode.url)
2402 channel_urls.add(episode.channel.url)
2404 # Tell the shownotes window that we have removed the episode
2405 if self.episode_shownotes_window is not None and \
2406 self.episode_shownotes_window.episode is not None and \
2407 self.episode_shownotes_window.episode.url == episode.url:
2408 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
2410 util.idle_add(finish_deletion, episode_urls, channel_urls)
2412 threading.Thread(target=thread_proc).start()
2414 return True
2416 def on_itemRemoveOldEpisodes_activate( self, widget):
2417 if gpodder.ui.maemo:
2418 columns = (
2419 ('maemo_remove_markup', None, None, _('Episode')),
2421 else:
2422 columns = (
2423 ('title_markup', None, None, _('Episode')),
2424 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2425 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2426 ('played_prop', None, None, _('Status')),
2427 ('age_prop', None, None, _('Downloaded')),
2430 selection_buttons = {
2431 _('Select played'): lambda episode: episode.is_played,
2432 _('Select older than %d days') % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2435 instructions = _('Select the episodes you want to delete:')
2437 episodes = []
2438 selected = []
2439 for channel in self.channels:
2440 for episode in channel.get_downloaded_episodes():
2441 # Disallow deletion of locked episodes that still exist
2442 if not episode.is_locked or not episode.file_exists():
2443 episodes.append(episode)
2444 # Automatically select played and file-less episodes
2445 selected.append(episode.is_played or \
2446 not episode.file_exists())
2448 gPodderEpisodeSelector(self.gPodder, title = _('Remove old episodes'), instructions = instructions, \
2449 episodes = episodes, selected = selected, columns = columns, \
2450 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2451 selection_buttons = selection_buttons, _config=self.config)
2453 def on_selected_episodes_status_changed(self):
2454 self.update_episode_list_icons(selected=True)
2455 self.update_podcast_list_model(selected=True)
2456 self.db.commit()
2458 def mark_selected_episodes_new(self):
2459 for episode in self.get_selected_episodes():
2460 episode.mark_new()
2461 self.on_selected_episodes_status_changed()
2463 def mark_selected_episodes_old(self):
2464 for episode in self.get_selected_episodes():
2465 episode.mark_old()
2466 self.on_selected_episodes_status_changed()
2468 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2469 for episode in self.get_selected_episodes():
2470 if toggle:
2471 episode.mark(is_played=not episode.is_played)
2472 else:
2473 episode.mark(is_played=new_value)
2474 self.on_selected_episodes_status_changed()
2476 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2477 for episode in self.get_selected_episodes():
2478 if toggle:
2479 episode.mark(is_locked=not episode.is_locked)
2480 else:
2481 episode.mark(is_locked=new_value)
2482 self.on_selected_episodes_status_changed()
2484 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2485 if self.active_channel is None:
2486 return
2488 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2489 self.active_channel.update_channel_lock()
2491 for episode in self.active_channel.get_all_episodes():
2492 episode.mark(is_locked=self.active_channel.channel_is_locked)
2494 self.update_podcast_list_model(selected=True)
2495 self.update_episode_list_icons(all=True)
2497 def on_itemUpdateChannel_activate(self, widget=None):
2498 if self.active_channel is None:
2499 title = _('No podcast selected')
2500 message = _('Please select a podcast in the podcasts list to update.')
2501 self.show_message( message, title, widget=self.treeChannels)
2502 return
2504 self.update_feed_cache(channels=[self.active_channel])
2506 def on_itemUpdate_activate(self, widget=None):
2507 if self.channels:
2508 self.update_feed_cache()
2509 else:
2510 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)
2512 def download_episode_list_paused(self, episodes):
2513 self.download_episode_list(episodes, True)
2515 def download_episode_list(self, episodes, add_paused=False):
2516 for episode in episodes:
2517 log('Downloading episode: %s', episode.title, sender = self)
2518 if not episode.was_downloaded(and_exists=True):
2519 task_exists = False
2520 for task in self.download_tasks_seen:
2521 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2522 self.download_queue_manager.add_task(task)
2523 self.enable_download_list_update()
2524 task_exists = True
2525 continue
2527 if task_exists:
2528 continue
2530 try:
2531 task = download.DownloadTask(episode, self.config)
2532 except Exception, e:
2533 self.show_message(_('Download error while downloading %s:\n\n%s') % (episode.title, str(e)), _('Download error'), important=True)
2534 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2535 continue
2537 if add_paused:
2538 task.status = task.PAUSED
2539 else:
2540 self.download_queue_manager.add_task(task)
2542 self.download_status_model.register_task(task)
2543 self.enable_download_list_update()
2545 def cancel_task_list(self, tasks):
2546 if not tasks:
2547 return
2549 for task in tasks:
2550 if task.status in (task.QUEUED, task.DOWNLOADING):
2551 task.status = task.CANCELLED
2552 elif task.status == task.PAUSED:
2553 task.status = task.CANCELLED
2554 # Call run, so the partial file gets deleted
2555 task.run()
2557 self.update_episode_list_icons([task.url for task in tasks])
2558 self.play_or_download()
2560 # Update the tab title and downloads list
2561 self.update_downloads_list()
2563 def new_episodes_show(self, episodes, notification=False):
2564 if gpodder.ui.maemo:
2565 columns = (
2566 ('maemo_markup', None, None, _('Episode')),
2568 show_notification = notification
2569 else:
2570 columns = (
2571 ('title_markup', None, None, _('Episode')),
2572 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2573 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2575 show_notification = False
2577 instructions = _('Select the episodes you want to download:')
2579 if self.new_episodes_window is not None:
2580 self.new_episodes_window.main_window.destroy()
2581 self.new_episodes_window = None
2583 def download_episodes_callback(episodes):
2584 self.new_episodes_window = None
2585 self.download_episode_list(episodes)
2587 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
2588 title=_('New episodes available'), \
2589 instructions=instructions, \
2590 episodes=episodes, \
2591 columns=columns, \
2592 selected_default=True, \
2593 stock_ok_button = 'gpodder-download', \
2594 callback=download_episodes_callback, \
2595 remove_callback=lambda e: e.mark_old(), \
2596 remove_action=_('Mark as old'), \
2597 remove_finished=self.episode_new_status_changed, \
2598 _config=self.config, \
2599 show_notification=show_notification)
2601 def on_itemDownloadAllNew_activate(self, widget, *args):
2602 if not self.offer_new_episodes():
2603 self.show_message(_('Please check for new episodes later.'), \
2604 _('No new episodes available'), widget=self.btnUpdateFeeds)
2606 def get_new_episodes(self, channels=None):
2607 if channels is None:
2608 channels = self.channels
2609 episodes = []
2610 for channel in channels:
2611 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2612 episodes.append(episode)
2614 return episodes
2616 def on_sync_to_ipod_activate(self, widget, episodes=None):
2617 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
2618 # The sync process might have updated the status of episodes,
2619 # therefore persist the database here to avoid losing data
2620 self.db.commit()
2622 def on_cleanup_ipod_activate(self, widget, *args):
2623 self.sync_ui.on_cleanup_device()
2625 def on_manage_device_playlist(self, widget):
2626 self.sync_ui.on_manage_device_playlist()
2628 def show_hide_tray_icon(self):
2629 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2630 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
2631 elif not self.config.display_tray_icon and self.tray_icon is not None:
2632 self.tray_icon.set_visible(False)
2633 del self.tray_icon
2634 self.tray_icon = None
2636 if self.config.minimize_to_tray and self.tray_icon:
2637 self.tray_icon.set_visible(self.is_iconified())
2638 elif self.tray_icon:
2639 self.tray_icon.set_visible(True)
2641 def on_itemShowToolbar_activate(self, widget):
2642 self.config.show_toolbar = self.itemShowToolbar.get_active()
2644 def on_itemShowDescription_activate(self, widget):
2645 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
2647 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
2648 self.config.podcast_list_hide_boring = toggleaction.get_active()
2649 if self.config.podcast_list_hide_boring:
2650 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2651 else:
2652 self.podcast_list_model.set_view_mode(-1)
2654 def on_item_view_podcasts_changed(self, radioaction, current):
2655 # Only on Fremantle
2656 if current == self.item_view_podcasts_all:
2657 self.podcast_list_model.set_view_mode(-1)
2658 elif current == self.item_view_podcasts_downloaded:
2659 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2660 elif current == self.item_view_podcasts_unplayed:
2661 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2663 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
2665 def on_item_view_episodes_changed(self, radioaction, current):
2666 if current == self.item_view_episodes_all:
2667 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
2668 elif current == self.item_view_episodes_undeleted:
2669 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
2670 elif current == self.item_view_episodes_downloaded:
2671 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2672 elif current == self.item_view_episodes_unplayed:
2673 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2675 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
2677 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
2678 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2680 def update_item_device( self):
2681 if not gpodder.ui.fremantle:
2682 if self.config.device_type != 'none':
2683 self.itemDevice.set_visible(True)
2684 self.itemDevice.label = self.get_device_name()
2685 else:
2686 self.itemDevice.set_visible(False)
2688 def properties_closed( self):
2689 self.show_hide_tray_icon()
2690 self.update_item_device()
2691 if gpodder.ui.maemo:
2692 selection = self.treeAvailable.get_selection()
2693 if self.config.maemo_enable_gestures or \
2694 self.config.enable_fingerscroll:
2695 selection.set_mode(gtk.SELECTION_SINGLE)
2696 else:
2697 selection.set_mode(gtk.SELECTION_MULTIPLE)
2699 def on_itemPreferences_activate(self, widget, *args):
2700 gPodderPreferences(self.gPodder, _config=self.config, \
2701 callback_finished=self.properties_closed, \
2702 user_apps_reader=self.user_apps_reader, \
2703 mygpo_login=lambda: self.require_my_gpodder_authentication(force_dialog=True))
2705 def on_itemDependencies_activate(self, widget):
2706 gPodderDependencyManager(self.gPodder)
2708 def require_my_gpodder_authentication(self, force_dialog=False):
2709 if force_dialog or (not self.config.my_gpodder_username or not self.config.my_gpodder_password):
2710 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'))
2711 if success:
2712 self.config.my_gpodder_username, self.config.my_gpodder_password = authentication
2713 return True
2714 else:
2715 return False
2717 return True
2719 def on_download_from_mygpo(self, widget=None):
2720 if self.require_my_gpodder_authentication():
2721 client = my.MygPodderClient(self.config.my_gpodder_service, \
2722 self.config.my_gpodder_username, self.config.my_gpodder_password)
2723 opml_data = client.download_subscriptions()
2724 if len(opml_data) > 0:
2725 fp = open(gpodder.subscription_file, 'w')
2726 fp.write(opml_data)
2727 fp.close()
2728 (added, skipped) = (0, 0)
2729 i = opml.Importer(gpodder.subscription_file)
2731 existing = [c.url for c in self.channels]
2732 urls = [item['url'] for item in i.items if item['url'] not in existing]
2734 skipped = len(i.items) - len(urls)
2735 added = len(urls)
2737 self.add_podcast_list(urls)
2738 if added > 0:
2739 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'), widget=self.treeChannels)
2740 elif widget is not None:
2741 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget=self.treeChannels)
2742 else:
2743 self.config.my_gpodder_password = ''
2744 self.on_download_from_mygpo(widget)
2745 else:
2746 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2748 def on_upload_to_mygpo(self, widget):
2749 if self.require_my_gpodder_authentication():
2750 client = my.MygPodderClient(self.config.my_gpodder_service, \
2751 self.config.my_gpodder_username, self.config.my_gpodder_password)
2752 self.save_channels_opml()
2753 success, messages = client.upload_subscriptions(gpodder.subscription_file)
2754 if widget is not None:
2755 if not success:
2756 self.show_message('\n'.join(messages), _('Results of upload'), important=True)
2757 self.config.my_gpodder_password = ''
2758 self.on_upload_to_mygpo(widget)
2759 else:
2760 self.show_message('\n'.join(messages), _('Results of upload'), widget=self.treeChannels)
2761 elif not success:
2762 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2763 elif widget is not None:
2764 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2766 def on_itemAddChannel_activate(self, widget=None):
2767 gPodderAddPodcast(self.gPodder, \
2768 add_urls_callback=self.add_podcast_list)
2770 def on_itemEditChannel_activate(self, widget, *args):
2771 if self.active_channel is None:
2772 title = _('No podcast selected')
2773 message = _('Please select a podcast in the podcasts list to edit.')
2774 self.show_message( message, title, widget=self.treeChannels)
2775 return
2777 callback_closed = lambda: self.update_podcast_list_model(selected=True)
2778 gPodderChannel(self.main_window, \
2779 channel=self.active_channel, \
2780 callback_closed=callback_closed, \
2781 cover_downloader=self.cover_downloader)
2783 def on_itemRemoveChannel_activate(self, widget, *args):
2784 if self.active_channel is None:
2785 title = _('No podcast selected')
2786 message = _('Please select a podcast in the podcasts list to remove.')
2787 self.show_message( message, title, widget=self.treeChannels)
2788 return
2790 try:
2791 if gpodder.ui.desktop:
2792 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2793 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2794 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2796 title = _('Remove podcast and episodes?')
2797 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2799 dialog.set_title(title)
2800 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2802 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2803 dialog.vbox.pack_start(cb_ask)
2804 cb_ask.show_all()
2805 result = (dialog.run() == gtk.RESPONSE_YES)
2806 keep_episodes = cb_ask.get_active()
2807 dialog.destroy()
2808 elif gpodder.ui.diablo:
2809 result = self.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2810 keep_episodes = False
2811 elif gpodder.ui.fremantle:
2812 result = True
2813 keep_episodes = False
2815 if result:
2816 progress = ProgressIndicator(_('Removing podcast'), \
2817 _('Please wait while the podcast is removed'), \
2818 parent=self.main_window)
2820 def finish_deletion(select_url):
2821 # Re-load the channels and select the desired new channel
2822 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2823 progress.on_finished()
2825 def thread_proc():
2826 # delete downloaded episodes only if checkbox is unchecked
2827 if keep_episodes:
2828 log('Not removing downloaded episodes', sender=self)
2829 else:
2830 progress.on_message(_('Removing downloaded episodes'))
2831 self.active_channel.remove_downloaded()
2833 # Clean up downloads and download directories
2834 self.clean_up_downloads()
2836 # cancel any active downloads from this channel
2837 for episode in self.active_channel.get_all_episodes():
2838 util.idle_add(self.download_status_model.cancel_by_url,
2839 episode.url)
2841 # get the URL of the podcast we want to select next
2842 position = self.channels.index(self.active_channel)
2843 if position == len(self.channels)-1:
2844 # this is the last podcast, so select the URL
2845 # of the item before this one (i.e. the "new last")
2846 select_url = self.channels[position-1].url
2847 else:
2848 # there is a podcast after the deleted one, so
2849 # we simply select the one that comes after it
2850 select_url = self.channels[position+1].url
2852 # Remove the channel
2853 progress.on_message(_('Cleaning up database'))
2854 self.active_channel.delete(purge=not keep_episodes)
2855 self.channels.remove(self.active_channel)
2856 self.channel_list_changed = True
2857 self.save_channels_opml()
2859 # The remaining stuff is to be done in the GTK main thread
2860 util.idle_add(finish_deletion, select_url)
2862 threading.Thread(target=thread_proc).start()
2863 except:
2864 log('There has been an error removing the channel.', traceback=True, sender=self)
2865 self.update_podcasts_tab()
2867 def get_opml_filter(self):
2868 filter = gtk.FileFilter()
2869 filter.add_pattern('*.opml')
2870 filter.add_pattern('*.xml')
2871 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2872 return filter
2874 def on_item_import_from_file_activate(self, widget, filename=None):
2875 if filename is None:
2876 if gpodder.ui.desktop or gpodder.ui.fremantle:
2877 # FIXME: Hildonization on Fremantle
2878 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2879 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2880 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2881 elif gpodder.ui.diablo:
2882 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2883 dlg.set_filter(self.get_opml_filter())
2884 response = dlg.run()
2885 filename = None
2886 if response == gtk.RESPONSE_OK:
2887 filename = dlg.get_filename()
2888 dlg.destroy()
2890 if filename is not None:
2891 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2892 custom_title=_('Import podcasts from OPML file'), \
2893 add_urls_callback=self.add_podcast_list, \
2894 hide_url_entry=True)
2895 dir.download_opml_file(filename)
2897 def on_itemExportChannels_activate(self, widget, *args):
2898 if not self.channels:
2899 title = _('Nothing to export')
2900 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2901 self.show_message(message, title, widget=self.treeChannels)
2902 return
2904 if gpodder.ui.desktop or gpodder.ui.fremantle:
2905 # FIXME: Hildonization on Fremantle
2906 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2907 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2908 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2909 elif gpodder.ui.diablo:
2910 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2911 dlg.set_filter(self.get_opml_filter())
2912 response = dlg.run()
2913 if response == gtk.RESPONSE_OK:
2914 filename = dlg.get_filename()
2915 dlg.destroy()
2916 exporter = opml.Exporter( filename)
2917 if exporter.write(self.channels):
2918 if len(self.channels) == 1:
2919 title = _('One subscription exported')
2920 else:
2921 title = _('%d subscriptions exported') % len(self.channels)
2922 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
2923 else:
2924 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
2925 else:
2926 dlg.destroy()
2928 def on_itemImportChannels_activate(self, widget, *args):
2929 if gpodder.ui.fremantle:
2930 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
2931 self.config.toplist_url, \
2932 self.config.opml_url, \
2933 self.add_podcast_list, \
2934 self.on_itemAddChannel_activate, \
2935 self.on_download_from_mygpo, \
2936 self.show_text_edit_dialog)
2937 else:
2938 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
2939 add_urls_callback=self.add_podcast_list)
2940 util.idle_add(dir.download_opml_file, self.config.opml_url)
2942 def on_homepage_activate(self, widget, *args):
2943 util.open_website(gpodder.__url__)
2945 def on_wiki_activate(self, widget, *args):
2946 util.open_website('http://wiki.gpodder.org/')
2948 def on_bug_tracker_activate(self, widget, *args):
2949 if gpodder.ui.maemo:
2950 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2951 else:
2952 util.open_website('http://bugs.gpodder.org/')
2954 def on_shop_activate(self, widget, *args):
2955 util.open_website('http://gpodder.org/shop')
2957 def on_wishlist_activate(self, widget, *args):
2958 util.open_website('http://amzn.com/w/2L04WZKX274VB')
2960 def on_item_support_activate(self, widget):
2961 util.open_website('http://gpodder.org/donate')
2963 def on_itemAbout_activate(self, widget, *args):
2964 dlg = gtk.AboutDialog()
2965 dlg.set_name('gPodder')
2966 dlg.set_version(gpodder.__version__)
2967 dlg.set_copyright(gpodder.__copyright__)
2968 dlg.set_comments(_('A podcast client with focus on usability'))
2969 if not gpodder.ui.fremantle:
2970 # Disable the URL label in Fremantle because of style issues
2971 dlg.set_website(gpodder.__url__)
2972 dlg.set_translator_credits( _('translator-credits'))
2973 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2975 if gpodder.ui.desktop:
2976 # For the "GUI" version, we add some more
2977 # items to the about dialog (credits and logo)
2978 app_authors = [
2979 _('Maintainer:'),
2980 'Thomas Perl <thpinfo.com>',
2983 if os.path.exists(gpodder.credits_file):
2984 credits = open(gpodder.credits_file).read().strip().split('\n')
2985 app_authors += ['', _('Patches, bug reports and donations by:')]
2986 app_authors += credits
2988 dlg.set_authors(app_authors)
2989 try:
2990 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
2991 except:
2992 dlg.set_logo_icon_name('gpodder')
2993 elif gpodder.ui.fremantle:
2994 for parent in dlg.vbox.get_children():
2995 for child in parent.get_children():
2996 if isinstance(child, gtk.Label):
2997 child.set_selectable(False)
2998 child.set_alignment(0.0, 0.5)
3000 dlg.run()
3002 def on_wNotebook_switch_page(self, widget, *args):
3003 page_num = args[1]
3004 if gpodder.ui.maemo:
3005 self.tool_downloads.set_active(page_num == 1)
3006 page = self.wNotebook.get_nth_page(page_num)
3007 tab_label = self.wNotebook.get_tab_label(page).get_text()
3008 if page_num == 0 and self.active_channel is not None:
3009 self.set_title(self.active_channel.title)
3010 else:
3011 self.set_title(tab_label)
3012 if page_num == 0:
3013 self.play_or_download()
3014 self.menuChannels.set_sensitive(True)
3015 self.menuSubscriptions.set_sensitive(True)
3016 # The message area in the downloads tab should be hidden
3017 # when the user switches away from the downloads tab
3018 if self.message_area is not None:
3019 self.message_area.hide()
3020 self.message_area = None
3021 else:
3022 self.menuChannels.set_sensitive(False)
3023 self.menuSubscriptions.set_sensitive(False)
3024 if gpodder.ui.desktop:
3025 self.toolDownload.set_sensitive(False)
3026 self.toolPlay.set_sensitive(False)
3027 self.toolTransfer.set_sensitive(False)
3028 self.toolCancel.set_sensitive(False)
3030 def on_treeChannels_row_activated(self, widget, path, *args):
3031 # double-click action of the podcast list or enter
3032 self.treeChannels.set_cursor(path)
3034 def on_treeChannels_cursor_changed(self, widget, *args):
3035 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3037 if model is not None and iter is not None:
3038 old_active_channel = self.active_channel
3039 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3041 if self.active_channel == old_active_channel:
3042 return
3044 if gpodder.ui.maemo:
3045 self.set_title(self.active_channel.title)
3046 self.itemEditChannel.set_visible(True)
3047 self.itemRemoveChannel.set_visible(True)
3048 else:
3049 self.active_channel = None
3050 self.itemEditChannel.set_visible(False)
3051 self.itemRemoveChannel.set_visible(False)
3053 self.update_episode_list_model()
3055 def on_btnEditChannel_clicked(self, widget, *args):
3056 self.on_itemEditChannel_activate( widget, args)
3058 def get_selected_episodes(self):
3059 """Get a list of selected episodes from treeAvailable"""
3060 selection = self.treeAvailable.get_selection()
3061 model, paths = selection.get_selected_rows()
3063 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3064 return episodes
3066 def on_transfer_selected_episodes(self, widget):
3067 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3069 def on_playback_selected_episodes(self, widget):
3070 self.playback_episodes(self.get_selected_episodes())
3072 def on_shownotes_selected_episodes(self, widget):
3073 episodes = self.get_selected_episodes()
3074 if episodes:
3075 episode = episodes.pop(0)
3076 self.show_episode_shownotes(episode)
3077 else:
3078 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3080 def on_download_selected_episodes(self, widget):
3081 episodes = self.get_selected_episodes()
3082 self.download_episode_list(episodes)
3083 self.update_episode_list_icons([episode.url for episode in episodes])
3084 self.play_or_download()
3086 def on_treeAvailable_row_activated(self, widget, path, view_column):
3087 """Double-click/enter action handler for treeAvailable"""
3088 # We should only have one one selected as it was double clicked!
3089 e = self.get_selected_episodes()[0]
3091 if (self.config.double_click_episode_action == 'download'):
3092 # If the episode has already been downloaded and exists then play it
3093 if e.was_downloaded(and_exists=True):
3094 self.playback_episodes(self.get_selected_episodes())
3095 # else download it if it is not already downloading
3096 elif not self.episode_is_downloading(e):
3097 self.download_episode_list([e])
3098 self.update_episode_list_icons([e.url])
3099 self.play_or_download()
3100 elif (self.config.double_click_episode_action == 'stream'):
3101 # If we happen to have downloaded this episode simple play it
3102 if e.was_downloaded(and_exists=True):
3103 self.playback_episodes(self.get_selected_episodes())
3104 # else if streaming is possible stream it
3105 elif self.streaming_possible():
3106 self.playback_episodes(self.get_selected_episodes())
3107 else:
3108 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3109 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3110 else:
3111 # default action is to display show notes
3112 self.on_shownotes_selected_episodes(widget)
3114 def show_episode_shownotes(self, episode):
3115 if self.episode_shownotes_window is None:
3116 log('First-time use of episode window --- creating', sender=self)
3117 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3118 _download_episode_list=self.download_episode_list, \
3119 _playback_episodes=self.playback_episodes, \
3120 _delete_episode_list=self.delete_episode_list, \
3121 _episode_list_status_changed=self.episode_list_status_changed, \
3122 _cancel_task_list=self.cancel_task_list, \
3123 _episode_is_downloading=self.episode_is_downloading, \
3124 _streaming_possible=self.streaming_possible())
3125 self.episode_shownotes_window.show(episode)
3126 if self.episode_is_downloading(episode):
3127 self.update_downloads_list()
3129 def restart_auto_update_timer(self):
3130 if self._auto_update_timer_source_id is not None:
3131 log('Removing existing auto update timer.', sender=self)
3132 gobject.source_remove(self._auto_update_timer_source_id)
3133 self._auto_update_timer_source_id = None
3135 if self.config.auto_update_feeds:
3136 interval = 60*1000*self.config.auto_update_frequency
3137 log('Setting up auto update timer with interval %d.', \
3138 self.config.auto_update_frequency, sender=self)
3139 self._auto_update_timer_source_id = gobject.timeout_add(\
3140 interval, self._on_auto_update_timer)
3142 def _on_auto_update_timer(self):
3143 log('Auto update timer fired.', sender=self)
3144 self.update_feed_cache(force_update=True)
3145 return True
3147 def on_treeDownloads_row_activated(self, widget, *args):
3148 # Use the standard way of working on the treeview
3149 selection = self.treeDownloads.get_selection()
3150 (model, paths) = selection.get_selected_rows()
3151 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3153 for tree_row_reference, task in selected_tasks:
3154 if task.status in (task.DOWNLOADING, task.QUEUED):
3155 task.status = task.PAUSED
3156 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3157 self.download_queue_manager.add_task(task)
3158 self.enable_download_list_update()
3159 elif task.status == task.DONE:
3160 model.remove(model.get_iter(tree_row_reference.get_path()))
3162 self.play_or_download()
3164 # Update the tab title and downloads list
3165 self.update_downloads_list()
3167 def on_item_cancel_download_activate(self, widget):
3168 if self.wNotebook.get_current_page() == 0:
3169 selection = self.treeAvailable.get_selection()
3170 (model, paths) = selection.get_selected_rows()
3171 urls = [model.get_value(model.get_iter(path), \
3172 self.episode_list_model.C_URL) for path in paths]
3173 selected_tasks = [task for task in self.download_tasks_seen \
3174 if task.url in urls]
3175 else:
3176 selection = self.treeDownloads.get_selection()
3177 (model, paths) = selection.get_selected_rows()
3178 selected_tasks = [model.get_value(model.get_iter(path), \
3179 self.download_status_model.C_TASK) for path in paths]
3180 self.cancel_task_list(selected_tasks)
3182 def on_btnCancelAll_clicked(self, widget, *args):
3183 self.cancel_task_list(self.download_tasks_seen)
3185 def on_btnDownloadedDelete_clicked(self, widget, *args):
3186 if self.wNotebook.get_current_page() == 1:
3187 # Downloads tab visibile - skip (for now)
3188 return
3190 episodes = self.get_selected_episodes()
3191 self.delete_episode_list(episodes)
3193 def on_key_press(self, widget, event):
3194 # Allow tab switching with Ctrl + PgUp/PgDown
3195 if event.state & gtk.gdk.CONTROL_MASK:
3196 if event.keyval == gtk.keysyms.Page_Up:
3197 self.wNotebook.prev_page()
3198 return True
3199 elif event.keyval == gtk.keysyms.Page_Down:
3200 self.wNotebook.next_page()
3201 return True
3203 # After this code we only handle Maemo hardware keys,
3204 # so if we are not a Maemo app, we don't do anything
3205 if not gpodder.ui.maemo:
3206 return False
3208 diff = 0
3209 if event.keyval == gtk.keysyms.F7: #plus
3210 diff = 1
3211 elif event.keyval == gtk.keysyms.F8: #minus
3212 diff = -1
3214 if diff != 0 and not self.currently_updating:
3215 selection = self.treeChannels.get_selection()
3216 (model, iter) = selection.get_selected()
3217 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3218 selection.select_path(new_path)
3219 self.treeChannels.set_cursor(new_path)
3220 return True
3222 return False
3224 def on_iconify(self):
3225 if self.tray_icon:
3226 self.gPodder.set_skip_taskbar_hint(True)
3227 if self.config.minimize_to_tray:
3228 self.tray_icon.set_visible(True)
3229 else:
3230 self.gPodder.set_skip_taskbar_hint(False)
3232 def on_uniconify(self):
3233 if self.tray_icon:
3234 self.gPodder.set_skip_taskbar_hint(False)
3235 if self.config.minimize_to_tray:
3236 self.tray_icon.set_visible(False)
3237 else:
3238 self.gPodder.set_skip_taskbar_hint(False)
3240 def uniconify_main_window(self):
3241 if self.is_iconified():
3242 self.gPodder.present()
3244 def iconify_main_window(self):
3245 if not self.is_iconified():
3246 self.gPodder.iconify()
3248 def update_podcasts_tab(self):
3249 if len(self.channels):
3250 if gpodder.ui.fremantle:
3251 self.button_podcasts.set_value(_('%d subscriptions') % len(self.channels))
3252 else:
3253 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3254 else:
3255 if gpodder.ui.fremantle:
3256 self.button_podcasts.set_value(_('No subscriptions'))
3257 else:
3258 self.label2.set_text(_('Podcasts'))
3260 @dbus.service.method(gpodder.dbus_interface)
3261 def show_gui_window(self):
3262 self.gPodder.present()
3264 @dbus.service.method(gpodder.dbus_interface)
3265 def subscribe_to_url(self, url):
3266 gPodderAddPodcast(self.gPodder,
3267 add_urls_callback=self.add_podcast_list,
3268 preset_url=url)
3270 @dbus.service.method(gpodder.dbus_interface)
3271 def mark_episode_played(self, filename):
3272 if filename is None:
3273 return False
3275 for channel in self.channels:
3276 for episode in channel.get_all_episodes():
3277 fn = episode.local_filename(create=False, check_only=True)
3278 if fn == filename:
3279 episode.mark(is_played=True)
3280 self.db.commit()
3281 self.update_episode_list_icons([episode.url])
3282 self.update_podcast_list_model([episode.channel.url])
3283 return True
3285 return False
3288 def main(options=None):
3289 gobject.threads_init()
3290 gobject.set_application_name('gPodder')
3292 if gpodder.ui.maemo:
3293 # Try to enable the custom icon theme for gPodder on Maemo
3294 settings = gtk.settings_get_default()
3295 settings.set_string_property('gtk-icon-theme-name', \
3296 'gpodder', __file__)
3297 # Extend the search path for the optified icon theme (Maemo 5)
3298 icon_theme = gtk.icon_theme_get_default()
3299 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
3301 gtk.window_set_default_icon_name('gpodder')
3302 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3304 try:
3305 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3306 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3307 except dbus.exceptions.DBusException, dbe:
3308 log('Warning: Cannot get "on the bus".', traceback=True)
3309 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3310 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3311 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3312 dlg.set_title('gPodder')
3313 dlg.run()
3314 dlg.destroy()
3315 sys.exit(0)
3317 util.make_directory(gpodder.home)
3318 gpodder.load_plugins()
3320 config = UIConfig(gpodder.config_file)
3322 if gpodder.ui.diablo:
3323 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3324 # folder exists there (allow moving "gpodder" between SD cards or USB)
3325 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3326 if not os.path.exists(config.download_dir):
3327 log('Downloads might have been moved. Trying to locate them...')
3328 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3329 dir = os.path.join(basedir, 'gpodder')
3330 if os.path.exists(dir):
3331 log('Downloads found in: %s', dir)
3332 config.download_dir = dir
3333 break
3334 else:
3335 log('Downloads NOT FOUND in %s', dir)
3337 if config.enable_fingerscroll:
3338 BuilderWidget.use_fingerscroll = True
3339 elif gpodder.ui.fremantle:
3340 config.on_quit_ask = False
3342 gp = gPodder(bus_name, config)
3344 # Handle options
3345 if options.subscribe:
3346 util.idle_add(gp.subscribe_to_url, options.subscribe)
3348 gp.run()