Threaded feed updater (bug 153)
This changeset is based on gPodder-r777-threaded-feed-updater.patch, plus: config.py: Add new config option 'max_simulaneous_feeds_updating'. By default it's set to 3, otherwise on slower devices the gui might become un-responsive during an update because of the CPU resources required by python-feedparser. Add 'color_updating_feeds' option for users that want to manually set it. gui.py: Make channels that are updating a different color, this gives the user a better idea of what's going on. Populate the update progress bar with a message and clear the previous fraction setting before starting a feed update. Colors dict for feed updates, default color is "None", so we don't need to do any fancy color detection (for all different GTK themes). libpodcasts.py: Make the channel's iter available as podcastChannel.iter. Don't use the <span foreground=...> markup tag to set the channel's text color, use the gtk.CellRendererText 'foreground' attribute. This allows the text color to be easily changed by modifying the channel model. Require the colors dict to be passed to channels_to_model. For Maemo, add "Update selected podcast" menu item to the Subscriptions menu, because the context menu isn't easily reached at the moment (no right-click).
This commit is contained in:
parent
9d8b9baaaf
commit
15a7bd56eb
4 changed files with 155 additions and 43 deletions
|
@ -268,6 +268,33 @@
|
|||
</widget>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkImageMenuItem" id="itemUpdateChannel">
|
||||
<property name="visible">False</property>
|
||||
<property name="label" translatable="yes">Update selected podcast</property>
|
||||
<property name="use_underline">True</property>
|
||||
<signal name="activate" handler="on_itemUpdateChannel_activate" last_modification_time="Mon, 1 Sep 2008 15:26:55 GMT"/>
|
||||
|
||||
<child internal-child="image">
|
||||
<widget class="GtkImage" id="image4586">
|
||||
<property name="visible">True</property>
|
||||
<property name="stock">gtk-refresh</property>
|
||||
<property name="icon_size">1</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="yalign">0.5</property>
|
||||
<property name="xpad">0</property>
|
||||
<property name="ypad">0</property>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkSeparatorMenuItem" id="UpdateChannelSeparator">
|
||||
<property name="visible">False</property>
|
||||
</widget>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkImageMenuItem" id="item_import_from_file">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -116,6 +116,8 @@ gPodderSettings = {
|
|||
'podcast_list_icon_size': (int, 32),
|
||||
'cmd_all_downloads_complete': (str, ''),
|
||||
'cmd_download_complete': (str, ''),
|
||||
'max_simulaneous_feeds_updating': (int, 3),
|
||||
'color_updating_feeds': (str, '#7db023'),
|
||||
|
||||
# Hide the cover/pill from the podcast sidebar when it gets too small
|
||||
'podcast_sidebar_save_space': (bool, False),
|
||||
|
|
|
@ -35,6 +35,7 @@ from xml.sax import saxutils
|
|||
|
||||
from threading import Event
|
||||
from threading import Thread
|
||||
from threading import Semaphore
|
||||
from string import strip
|
||||
|
||||
import gpodder
|
||||
|
@ -274,6 +275,9 @@ class gPodder(GladeWidget):
|
|||
self.window.connect('delete-event', self.on_gPodder_delete_event)
|
||||
self.window.connect('window-state-event', self.window_state_event)
|
||||
self.window.connect('key-press-event', self.on_key_press)
|
||||
|
||||
self.itemUpdateChannel.show()
|
||||
self.UpdateChannelSeparator.show()
|
||||
|
||||
# Give toolbar to the hildon window
|
||||
self.toolbar.parent.remove(self.toolbar)
|
||||
|
@ -369,9 +373,11 @@ class gPodder(GladeWidget):
|
|||
self.cell_channel_icon = iconcell
|
||||
|
||||
namecell = gtk.CellRendererText()
|
||||
namecell.set_property('foreground-set', True)
|
||||
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
namecolumn.pack_start( namecell, True)
|
||||
namecolumn.add_attribute( namecell, 'markup', 2)
|
||||
namecolumn.add_attribute( namecell, 'foreground', 8)
|
||||
|
||||
iconcell = gtk.CellRendererPixbuf()
|
||||
iconcell.set_property('xalign', 1.0)
|
||||
|
@ -502,7 +508,16 @@ class gPodder(GladeWidget):
|
|||
# Set the "Device" menu item for the first time
|
||||
self.update_item_device()
|
||||
|
||||
# Set up default channel colors
|
||||
self.channel_colors = {
|
||||
'default': None,
|
||||
'updating': gl.config.color_updating_feeds,
|
||||
'parse_error': '#ff0000',
|
||||
}
|
||||
|
||||
# Now, update the feed cache, when everything's in place
|
||||
self.btnUpdateFeeds.show_all()
|
||||
self.updated_feeds = 0
|
||||
self.updating_feed_cache = False
|
||||
self.feed_cache_update_cancelled = False
|
||||
self.update_feed_cache(force_update=gl.config.update_on_startup)
|
||||
|
@ -705,6 +720,12 @@ class gPodder(GladeWidget):
|
|||
item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
|
||||
menu.append( item)
|
||||
|
||||
item = gtk.ImageMenuItem( _('Update Feed'))
|
||||
item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
|
||||
item.connect('activate', self.on_itemUpdateChannel_activate )
|
||||
item.set_sensitive( not self.updating_feed_cache )
|
||||
menu.append( item)
|
||||
|
||||
if gl.config.create_m3u_playlists:
|
||||
item = gtk.ImageMenuItem(_('Update M3U playlist'))
|
||||
item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
|
||||
|
@ -1150,7 +1171,7 @@ class gPodder(GladeWidget):
|
|||
selected_url = model.get_value(iter, 0)
|
||||
|
||||
rect = self.treeChannels.get_visible_rect()
|
||||
self.treeChannels.set_model(channels_to_model(self.channels, self.cover_cache, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size))
|
||||
self.treeChannels.set_model(channels_to_model(self.channels, self.channel_colors, self.cover_cache, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size))
|
||||
util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
|
||||
|
||||
try:
|
||||
|
@ -1270,29 +1291,17 @@ class gPodder(GladeWidget):
|
|||
self.show_message( message, title)
|
||||
else:
|
||||
self.show_message(_('There has been an error adding this podcast. Please see the log output for more information.'), _('Error adding podcast'))
|
||||
self.update_podcasts_tab()
|
||||
|
||||
def update_feed_cache_callback(self, progressbar, position, count, force_update):
|
||||
if position < len(self.channels):
|
||||
title = self.channels[position].title
|
||||
if force_update:
|
||||
progression = _('Updating %s (%d/%d)')%(title, position+1, count)
|
||||
else:
|
||||
progression = _('Loading %s (%d/%d)')%(title, position+1, count)
|
||||
progressbar.set_text(progression)
|
||||
if self.tray_icon:
|
||||
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
|
||||
self.update_podcasts_tab()
|
||||
|
||||
if count > 0:
|
||||
progressbar.set_fraction(float(position)/float(count))
|
||||
|
||||
def update_feed_cache_finish_callback(self, force_update=False, notify_no_new_episodes=False,
|
||||
select_url_afterwards=None):
|
||||
|
||||
def update_feed_cache_finish_callback(self, channels=None,
|
||||
notify_no_new_episodes=False, select_url_afterwards=None):
|
||||
|
||||
self.updating_feed_cache = False
|
||||
self.hboxUpdateFeeds.hide_all()
|
||||
self.btnUpdateFeeds.show_all()
|
||||
self.itemUpdate.set_sensitive(True)
|
||||
self.itemUpdateChannel.set_sensitive(True)
|
||||
|
||||
# If we want to select a specific podcast (via its URL)
|
||||
# after the update, we give it to updateComboBox here to
|
||||
|
@ -1301,7 +1310,7 @@ class gPodder(GladeWidget):
|
|||
|
||||
if self.tray_icon:
|
||||
self.tray_icon.set_status(None)
|
||||
if self.minimized and force_update:
|
||||
if self.minimized:
|
||||
new_episodes = []
|
||||
# look for new episodes to notify
|
||||
for channel in self.channels:
|
||||
|
@ -1330,46 +1339,100 @@ class gPodder(GladeWidget):
|
|||
return
|
||||
|
||||
# open the episodes selection dialog
|
||||
if force_update:
|
||||
self.on_itemDownloadAllNew_activate( self.gPodder)
|
||||
self.channels = load_channels()
|
||||
self.updateComboBox()
|
||||
if not self.feed_cache_update_cancelled:
|
||||
self.download_all_new(channels=channels)
|
||||
|
||||
def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
|
||||
if not force_update:
|
||||
self.channels = load_channels()
|
||||
else:
|
||||
is_cancelled_cb = lambda: self.feed_cache_update_cancelled
|
||||
self.channels = update_channels(callback_proc=callback_proc, callback_error=callback_error, is_cancelled_cb=is_cancelled_cb)
|
||||
def update_feed_cache_callback(self, progressbar, title, position, count):
|
||||
progression = _('Updated %s (%d/%d)')%(title, position+1, count)
|
||||
progressbar.set_text(progression)
|
||||
if self.tray_icon:
|
||||
self.tray_icon.set_status(
|
||||
self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
|
||||
if count > 0:
|
||||
progressbar.set_fraction(float(position)/float(count))
|
||||
|
||||
self.pbFeedUpdate.set_text(_('Building list...'))
|
||||
if finish_proc:
|
||||
def update_feed_cache_proc( self, channel, total_channels, semaphore,
|
||||
callback_proc, finish_proc):
|
||||
|
||||
semaphore.acquire()
|
||||
if not self.feed_cache_update_cancelled:
|
||||
try:
|
||||
channel.update()
|
||||
except:
|
||||
log('Darn SQLite LOCK!', sender=self, traceback=True)
|
||||
|
||||
# By the time we get here the update may have already been cancelled
|
||||
if not self.feed_cache_update_cancelled:
|
||||
callback_proc(channel.title, self.updated_feeds, total_channels)
|
||||
|
||||
self.updated_feeds += 1
|
||||
self.treeview_channel_set_color( channel, 'default' )
|
||||
channel.update_flag = False
|
||||
|
||||
semaphore.release()
|
||||
if self.updated_feeds == total_channels:
|
||||
finish_proc()
|
||||
|
||||
def on_btnCancelFeedUpdate_clicked(self, widget):
|
||||
self.pbFeedUpdate.set_text(_('Cancelling...'))
|
||||
self.feed_cache_update_cancelled = True
|
||||
|
||||
def update_feed_cache(self, force_update=True, notify_no_new_episodes=False, select_url_afterwards=None):
|
||||
def update_feed_cache(self, channels=None, force_update=True,
|
||||
notify_no_new_episodes=False, select_url_afterwards=None):
|
||||
|
||||
if self.updating_feed_cache:
|
||||
return
|
||||
|
||||
if not force_update:
|
||||
self.channels = load_channels()
|
||||
self.updateComboBox()
|
||||
return
|
||||
|
||||
self.updating_feed_cache = True
|
||||
self.itemUpdate.set_sensitive(False)
|
||||
self.itemUpdateChannel.set_sensitive(False)
|
||||
|
||||
if self.tray_icon:
|
||||
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
|
||||
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
|
||||
if len(channels) == 1:
|
||||
text = _('Updating %d feed.')
|
||||
else:
|
||||
text = _('Updating %d feeds.')
|
||||
self.pbFeedUpdate.set_text( text % len(channels))
|
||||
self.pbFeedUpdate.set_fraction(0)
|
||||
|
||||
# let's get down to business..
|
||||
callback_proc = lambda pos, count: util.idle_add(self.update_feed_cache_callback, self.pbFeedUpdate, pos, count, force_update)
|
||||
finish_proc = lambda: util.idle_add(self.update_feed_cache_finish_callback, force_update, notify_no_new_episodes, select_url_afterwards)
|
||||
callback_proc = lambda title, pos, count: util.idle_add(
|
||||
self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
|
||||
finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
|
||||
channels, notify_no_new_episodes, select_url_afterwards )
|
||||
|
||||
self.updated_feeds = 0
|
||||
self.feed_cache_update_cancelled = False
|
||||
self.btnUpdateFeeds.hide_all()
|
||||
self.hboxUpdateFeeds.show_all()
|
||||
semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
|
||||
|
||||
args = (force_update, callback_proc, self.notification, finish_proc)
|
||||
for channel in channels:
|
||||
self.treeview_channel_set_color( channel, 'updating' )
|
||||
channel.update_flag = True
|
||||
args = (channel, len(channels), semaphore, callback_proc, finish_proc)
|
||||
thread = Thread( target = self.update_feed_cache_proc, args = args)
|
||||
thread.start()
|
||||
|
||||
def treeview_channel_set_color( self, channel, color ):
|
||||
if color in self.channel_colors:
|
||||
self.treeChannels.get_model().set(channel.iter, 8,
|
||||
self.channel_colors[color])
|
||||
else:
|
||||
self.treeChannels.get_model().set(channel.iter, 8, color)
|
||||
|
||||
thread = Thread( target = self.update_feed_cache_proc, args = args)
|
||||
thread.start()
|
||||
|
||||
def on_gPodder_delete_event(self, widget, *args):
|
||||
"""Called when the GUI wants to close the window
|
||||
Displays a confirmation dialog (and closes/hides gPodder)
|
||||
|
@ -1528,6 +1591,9 @@ class gPodder(GladeWidget):
|
|||
def on_item_show_url_entry_activate(self, widget):
|
||||
gl.config.show_podcast_url_entry = self.item_show_url_entry.get_active()
|
||||
|
||||
def on_itemUpdateChannel_activate(self, widget=None):
|
||||
self.update_feed_cache(channels=[self.active_channel,])
|
||||
|
||||
def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
|
||||
restore_from = can_restore_from_opml()
|
||||
|
||||
|
@ -1614,8 +1680,13 @@ class gPodder(GladeWidget):
|
|||
self.show_message(message, title)
|
||||
|
||||
def on_itemDownloadAllNew_activate(self, widget, *args):
|
||||
self.download_all_new()
|
||||
|
||||
def download_all_new(self, channels=None):
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
episodes = []
|
||||
for channel in self.channels:
|
||||
for channel in channels:
|
||||
for episode in channel.get_new_episodes():
|
||||
episodes.append(episode)
|
||||
self.new_episodes_show(episodes)
|
||||
|
|
|
@ -193,6 +193,8 @@ class podcastChannel(object):
|
|||
self.pubDate = 0
|
||||
self.parse_error = None
|
||||
self.newest_pubdate_cached = None
|
||||
self.update_flag = False # channel is updating or to be updated
|
||||
self.iter = None
|
||||
|
||||
# should this channel be synced to devices? (ex: iPod)
|
||||
self.sync_to_devices = True
|
||||
|
@ -683,8 +685,9 @@ class podcastItem(object):
|
|||
|
||||
|
||||
|
||||
def channels_to_model(channels, cover_cache=None, max_width=0, max_height=0):
|
||||
new_model = gtk.ListStore(str, str, str, gtk.gdk.Pixbuf, int, gtk.gdk.Pixbuf, str, bool)
|
||||
def channels_to_model(channels, color_dict, cover_cache=None, max_width=0, max_height=0):
|
||||
new_model = gtk.ListStore( str, str, str, gtk.gdk.Pixbuf, int,
|
||||
gtk.gdk.Pixbuf, str, bool, str )
|
||||
|
||||
for channel in channels:
|
||||
count_downloaded = channel.stat(state=db.STATE_DOWNLOADED)
|
||||
|
@ -692,6 +695,7 @@ def channels_to_model(channels, cover_cache=None, max_width=0, max_height=0):
|
|||
count_unplayed = channel.stat(state=db.STATE_DOWNLOADED, is_played=False)
|
||||
|
||||
new_iter = new_model.append()
|
||||
channel.iter = new_iter
|
||||
new_model.set(new_iter, 0, channel.url)
|
||||
new_model.set(new_iter, 1, channel.title)
|
||||
|
||||
|
@ -703,13 +707,21 @@ def channels_to_model(channels, cover_cache=None, max_width=0, max_height=0):
|
|||
d.append(title_markup)
|
||||
if count_new:
|
||||
d.append('</span>')
|
||||
|
||||
description = ''.join(d+['\n', '<small>', description_markup, '</small>'])
|
||||
if channel.parse_error is not None:
|
||||
description = ''.join(['<span foreground="#ff0000">', description, '</span>'])
|
||||
new_model.set(new_iter, 6, channel.parse_error)
|
||||
|
||||
new_model.set(new_iter, 2, description)
|
||||
|
||||
if channel.parse_error is not None:
|
||||
new_model.set(new_iter, 6, channel.parse_error)
|
||||
color = color_dict['parse_error']
|
||||
else:
|
||||
color = color_dict['default']
|
||||
|
||||
if channel.update_flag:
|
||||
color = color_dict['updating']
|
||||
|
||||
new_model.set(new_iter, 8, color)
|
||||
|
||||
if count_unplayed > 0 or count_downloaded > 0:
|
||||
new_model.set(new_iter, 3, draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded)))
|
||||
new_model.set(new_iter, 7, True)
|
||||
|
|
Loading…
Reference in a new issue