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:
parent
a90c7c6073
commit
6bd7717bc9
98
src/gpodder/gtkui/interface/progress.py
Normal file
98
src/gpodder/gtkui/interface/progress.py
Normal 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)
|
||||
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue