Threaded podcast adding with new GUI dialog

Adding one or more podcasts shows a dialog with
progress information when the process takes longer
than one second.
This commit is contained in:
Thomas Perl 2009-09-12 15:32:10 +02:00
parent a90c7c6073
commit 6bd7717bc9
3 changed files with 245 additions and 98 deletions

View file

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import gtk
import gobject
import pango
import gpodder
_ = gpodder.gettext
from gpodder.gtkui.widgets import SpinningProgressIndicator
class ProgressIndicator(object):
# Delayed time until window is shown (for short operations)
DELAY = 1000
# Time between GUI updates after window creation
INTERVAL = 100
def __init__(self, title, subtitle=None, cancellable=False, parent=None):
self.title = title
self.subtitle = subtitle
self.cancellable = cancellable
self.parent = None
self.dialog = None
self.progressbar = None
self.indicator = None
self._initial_message = None
self._initial_progress = None
self.source_id = gobject.timeout_add(self.DELAY, self._create_progress)
def _create_progress(self):
self.dialog = gtk.MessageDialog(self.parent, \
0, 0, gtk.BUTTONS_CANCEL, self.subtitle or self.title)
self.dialog.set_title(self.title)
self.dialog.label.set_selectable(False)
self.dialog.set_response_sensitive(gtk.RESPONSE_CANCEL, \
self.cancellable)
self.progressbar = gtk.ProgressBar()
self.progressbar.set_ellipsize(pango.ELLIPSIZE_END)
# If the window is shown after the first update, set the progress
# info so that when the window appears, data is there already
if self._initial_progress is not None:
self.progressbar.set_fraction(self._initial_progress)
if self._initial_message is not None:
self.progressbar.set_text(self._initial_message)
self.dialog.vbox.add(self.progressbar)
self.indicator = SpinningProgressIndicator()
self.dialog.set_image(self.indicator)
self.dialog.show_all()
gobject.source_remove(self.source_id)
self.source_id = gobject.timeout_add(self.INTERVAL, self._update_gui)
return False
def _update_gui(self):
if self.indicator:
self.indicator.step_animation()
return True
def on_message(self, message):
if self.progressbar:
self.progressbar.set_text(message)
else:
self._initial_message = message
def on_progress(self, progress):
if self.progressbar:
self.progressbar.set_fraction(progress)
else:
self._initial_progress = progress
def on_finished(self):
if self.dialog is not None:
self.dialog.destroy()
gobject.source_remove(self.source_id)

View file

@ -139,3 +139,39 @@ class NotificationWindow(gtk.Window):
self._finished = True
return False
class SpinningProgressIndicator(gtk.Image):
# Progress indicator loading inspired by glchess from gnome-games-clutter
def __init__(self, size=32):
gtk.Image.__init__(self)
self._frames = []
self._frame_id = 0
# Load the progress indicator
icon_theme = gtk.icon_theme_get_default()
ICON = lambda x: x
try:
icon = icon_theme.load_icon(ICON('process-working'), size, 0)
width, height = icon.get_width(), icon.get_height()
if width < size or height < size:
size = min(width, height)
for row in range(height/size):
for column in range(width/size):
frame = icon.subpixbuf(column*size, row*size, size, size)
self._frames.append(frame)
# Remove the first frame (the "idle" icon)
if self._frames:
self._frames.pop(0)
self.step_animation()
except:
# FIXME: This is not very beautiful :/
self.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
def step_animation(self):
if len(self._frames) > 1:
self._frame_id += 1
if self._frame_id >= len(self._frames):
self._frame_id = 0
self.set_from_pixbuf(self._frames[self._frame_id])

View file

@ -114,6 +114,7 @@ else:
have_trayicon = False
from gpodder.gtkui.interface.welcome import gPodderWelcome
from gpodder.gtkui.interface.progress import ProgressIndicator
if gpodder.interface == gpodder.MAEMO:
import hildon
@ -1612,116 +1613,128 @@ class gPodder(BuilderWidget, dbus.service.Object):
error_messages = {}
redirections = {}
# After the initial sorting and splitting, try all queued podcasts
for url in queued:
log('QUEUE RUNNER: %s', url, sender=self)
try:
# The URL is valid and does not exist already - subscribe!
channel = PodcastChannel.load(self.db, url=url, create=True, \
authentication_tokens=auth_tokens.get(url, None), \
max_episodes=self.config.max_episodes_per_feed, \
download_dir=self.config.download_dir)
progress = ProgressIndicator(_('Adding podcasts'), \
_('Please wait while episode information is downloaded.'), \
parent=self.main_window)
try:
username, password = util.username_password_from_url(url)
except ValueError, ve:
username, password = (None, None)
def on_after_update():
progress.on_finished()
# Report already-existing subscriptions to the user
if existing:
title = _('Existing subscriptions skipped')
message = _('You are already subscribed to these podcasts:') \
+ '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
self.show_message(message, title, widget=self.treeChannels)
if username is not None and channel.username is None and \
password is not None and channel.password is None:
channel.username = username
channel.password = password
channel.save()
# Report subscriptions that require authentication
if authreq:
retry_podcasts = {}
for url in authreq:
title = _('Podcast requires authentication')
message = _('Please login to %s:') % (saxutils.escape(url),)
success, auth_tokens = self.show_login_dialog(title, message)
if success:
retry_podcasts[url] = auth_tokens
else:
# Stop asking the user for more login data
retry_podcasts = {}
for url in authreq:
error_messages[url] = _('Authentication failed')
failed.append(url)
break
self._update_cover(channel)
except feedcore.AuthenticationRequired:
if url in auth_tokens:
# Fail for wrong authentication data
error_messages[url] = _('Authentication failed')
failed.append(url)
# If we have authentication data to retry, do so here
if retry_podcasts:
self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
# Report website redirections
for url in redirections:
title = _('Website redirection detected')
message = _('The URL %s redirects to %s.') \
+ '\n\n' + _('Do you want to visit the website now?')
message = message % (url, redirections[url])
if self.show_confirmation(message, title):
util.open_website(error.data)
else:
# Queue for login dialog later
authreq.append(url)
continue
except feedcore.WifiLogin, error:
redirections[url] = error.data
failed.append(url)
error_messages[url] = _('Redirection detected')
continue
except Exception, e:
log('Subscription error: %s', e, traceback=True, sender=self)
error_messages[url] = str(e)
failed.append(url)
continue
assert channel is not None
worked.append(channel.url)
self.channels.append(channel)
self.channel_list_changed = True
# Report already-existing subscriptions to the user
if existing:
title = _('Existing subscriptions skipped')
message = _('You are already subscribed to these podcasts:') \
+ '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
self.show_message(message, title, widget=self.treeChannels)
# Report subscriptions that require authentication
if authreq:
retry_podcasts = {}
for url in authreq:
title = _('Podcast requires authentication')
message = _('Please login to %s:') % (saxutils.escape(url),)
success, auth_tokens = self.show_login_dialog(title, message)
if success:
retry_podcasts[url] = auth_tokens
else:
# Stop asking the user for more login data
retry_podcasts = {}
for url in authreq:
error_messages[url] = _('Authentication failed')
failed.append(url)
break
# If we have authentication data to retry, do so here
if retry_podcasts:
self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
# Report failed subscriptions to the user
if failed:
title = _('Could not add some podcasts')
message = _('Some podcasts could not be added to your list:') \
+ '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
error_messages.get(url, _('Unknown')))) for url in failed)
self.show_message(message, title, important=True)
# Report website redirections
for url in redirections:
title = _('Website redirection detected')
message = _('The URL %s redirects to %s.') \
+ '\n\n' + _('Do you want to visit the website now?')
message = message % (url, redirections[url])
if self.show_confirmation(message, title):
util.open_website(error.data)
else:
break
# If at least one podcast has been added, save and update all
if self.channel_list_changed:
self.save_channels_opml()
# Report failed subscriptions to the user
if failed:
title = _('Could not add some podcasts')
message = _('Some podcasts could not be added to your list:') \
+ '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
error_messages.get(url, _('Unknown')))) for url in failed)
self.show_message(message, title, important=True)
# If only one podcast was added, select it after the update
if len(worked) == 1:
url = worked[0]
else:
url = None
# If at least one podcast has been added, save and update all
if self.channel_list_changed:
self.save_channels_opml()
# Update the list of subscribed podcasts
self.update_feed_cache(force_update=False, select_url_afterwards=url)
self.update_podcasts_tab()
# If only one podcast was added, select it after the update
if len(worked) == 1:
url = worked[0]
else:
url = None
# Offer to download new episodes
self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
# Update the list of subscribed podcasts
self.update_feed_cache(force_update=False, select_url_afterwards=url)
self.update_podcasts_tab()
def thread_proc():
# After the initial sorting and splitting, try all queued podcasts
length = len(queued)
for index, url in enumerate(queued):
progress.on_progress(float(index)/float(length))
progress.on_message(url)
log('QUEUE RUNNER: %s', url, sender=self)
try:
# The URL is valid and does not exist already - subscribe!
channel = PodcastChannel.load(self.db, url=url, create=True, \
authentication_tokens=auth_tokens.get(url, None), \
max_episodes=self.config.max_episodes_per_feed, \
download_dir=self.config.download_dir)
# Offer to download new episodes
self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
try:
username, password = util.username_password_from_url(url)
except ValueError, ve:
username, password = (None, None)
if username is not None and channel.username is None and \
password is not None and channel.password is None:
channel.username = username
channel.password = password
channel.save()
self._update_cover(channel)
except feedcore.AuthenticationRequired:
if url in auth_tokens:
# Fail for wrong authentication data
error_messages[url] = _('Authentication failed')
failed.append(url)
else:
# Queue for login dialog later
authreq.append(url)
continue
except feedcore.WifiLogin, error:
redirections[url] = error.data
failed.append(url)
error_messages[url] = _('Redirection detected')
continue
except Exception, e:
log('Subscription error: %s', e, traceback=True, sender=self)
error_messages[url] = str(e)
failed.append(url)
continue
assert channel is not None
worked.append(channel.url)
self.channels.append(channel)
self.channel_list_changed = True
util.idle_add(on_after_update)
threading.Thread(target=thread_proc).start()
def save_channels_opml(self):
exporter = opml.Exporter(gpodder.subscription_file)