Improve podcast list updating in Gtk UI

This commit is contained in:
Thomas Perl 2011-07-16 20:51:26 +02:00
parent 71dc786d07
commit f5acd56d24
3 changed files with 146 additions and 176 deletions

View File

@ -42,8 +42,6 @@ import threading
import re
class Database(object):
UNICODE_TRANSLATE = {ord(u'ö'): u'o', ord(u'ä'): u'a', ord(u'ü'): u'u'}
TABLE_PODCAST = 'podcast'
TABLE_EPISODE = 'episode'
@ -86,33 +84,11 @@ class Database(object):
cur.close()
def db_sort_cmp(self, a, b):
"""
Compare two strings for sorting, including removing
a possible "The " prefix and converting umlauts to
normal characters so they can be sorted correctly.
(i.e. "Ö1" should not appear at the end of the list)
"""
try:
a = a.decode('utf-8', 'ignore').lower()
a = re.sub('^the ', '', a)
a = a.translate(self.UNICODE_TRANSLATE)
b = b.decode('utf-8', 'ignore').lower()
b = re.sub('^the ', '', b)
b = b.translate(self.UNICODE_TRANSLATE)
return cmp(a, b)
except:
logger.warn('Error comparing %s <=> %s', a, b, exc_info=True)
a = re.sub('^the ', '', a.lower())
b = re.sub('^the ', '', b.lower())
return cmp(a, b)
@property
def db(self):
if self._db is None:
self._db = sqlite.connect(self.database_file, check_same_thread=False)
self._db.text_factory = str
self._db.create_collation("UNICODE", self.db_sort_cmp)
# Check schema version, upgrade if necessary
schema.upgrade(self._db)
@ -174,7 +150,7 @@ class Database(object):
def load_podcasts(self, factory):
logger.info('Loading podcasts')
sql = 'SELECT * FROM %s ORDER BY title COLLATE UNICODE' % self.TABLE_PODCAST
sql = 'SELECT * FROM %s' % self.TABLE_PODCAST
with self.lock:
cur = self.cursor()

View File

@ -207,8 +207,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
for podcast in self.channels:
podcast.import_external_files()
self.channel_list_changed = True
# load list of user applications for audio playback
self.user_apps_reader = UserAppsReader(['audio', 'video'])
threading.Thread(target=self.user_apps_reader.read).start()
@ -218,9 +216,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
# Now, update the feed cache, when everything's in place
self.btnUpdateFeeds.show()
self.updating_feed_cache = False
self.feed_cache_update_cancelled = False
self.update_feed_cache(force_update=False)
self.update_podcast_list_model()
self.message_area = None
@ -482,7 +479,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
rewritten_url.new_url)
channel.url = rewritten_url.new_url
channel.save()
self.channel_list_changed = True
util.idle_add(self.update_episode_list_model)
break
@ -1149,7 +1145,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.restart_auto_update_timer()
elif name == 'podcast_list_view_all':
# Force a update of the podcast list model
self.channel_list_changed = True
self.update_podcast_list_model()
elif name == 'episode_list_columns':
self.update_episode_list_columns_visibility()
@ -1465,7 +1460,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
item = gtk.ImageMenuItem( _('Update podcast'))
item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
item.connect('activate', self.on_itemUpdateChannel_activate)
item.set_sensitive(not self.updating_feed_cache)
menu.append(item)
menu.append(gtk.SeparatorMenuItem())
@ -1945,9 +1939,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
selection = self.treeChannels.get_selection()
model, iter = selection.get_selected()
if self.config.podcast_list_view_all and not self.channel_list_changed:
if self.config.podcast_list_view_all:
# Update "all episodes" view in any case (if enabled)
self.podcast_list_model.update_first_row()
# List model length minus 2, because of "All" + separator
list_model_length = len(self.podcast_list_model) - 2
else:
list_model_length = len(self.podcast_list_model)
if selected:
# very cheap! only update selected channel
@ -1961,7 +1959,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
else:
# Otherwise just update the selected row (a podcast)
self.podcast_list_model.update_by_filter_iter(iter)
elif not self.channel_list_changed:
elif list_model_length == len(self.channels):
# we can keep the model, but have to update some
if urls is None:
# still cheaper than reloading the whole list
@ -1995,7 +1993,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.on_treeChannels_cursor_changed(self.treeChannels)
except:
logger.error('Cannot select podcast in list', exc_info=True)
self.channel_list_changed = False
def on_episode_list_filter_changed(self, has_episodes):
pass # XXX: Remove?
@ -2111,31 +2108,30 @@ class gPodder(BuilderWidget, dbus.service.Object):
# Upload subscription changes to gpodder.net
self.mygpo_client.on_subscribe(worked)
# If at least one podcast has been added, save and update all
if self.channel_list_changed:
# Fix URLs if mygpo has rewritten them
self.rewrite_urls_mygpo()
# Fix URLs if mygpo has rewritten them
self.rewrite_urls_mygpo()
# If only one podcast was added, select it after the update
if len(worked) == 1:
url = worked[0]
else:
url = None
# If only one podcast was added, select it after the update
if len(worked) == 1:
url = worked[0]
else:
url = None
# Update the list of subscribed podcasts
self.update_feed_cache(force_update=False, select_url_afterwards=url)
# Update the list of subscribed podcasts
self.channels.sort(key=Model.podcast_sort_key)
self.update_podcast_list_model(select_url=url)
# Offer to download new episodes
episodes = []
for podcast in self.channels:
if podcast.url in worked:
episodes.extend(podcast.get_all_episodes())
# Offer to download new episodes
episodes = []
for podcast in self.channels:
if podcast.url in worked:
episodes.extend(podcast.get_all_episodes())
if episodes:
episodes = list(Model.sort_episodes_by_pubdate(episodes, \
reverse=True))
self.new_episodes_show(episodes, \
selected=[e.check_is_new() for e in episodes])
if episodes:
episodes = list(Model.sort_episodes_by_pubdate(episodes, \
reverse=True))
self.new_episodes_show(episodes, \
selected=[e.check_is_new() for e in episodes])
def thread_proc():
@ -2186,7 +2182,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
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()
@ -2252,96 +2248,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
indicator.on_finished()
self.db.commit()
def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
self.db.commit()
self.updating_feed_cache = False
self.channels = Model.get_podcasts(self.db)
# Process received episode actions for all updated URLs
self.process_received_episode_actions(updated_urls)
self.channel_list_changed = True
self.update_podcast_list_model(select_url=select_url_afterwards)
# Only search for new episodes in podcasts that have been
# updated, not in other podcasts (for single-feed updates)
episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
if self.feed_cache_update_cancelled:
# The user decided to abort the feed update
self.show_update_feeds_buttons()
elif not episodes:
# Nothing new here - but inform the user
self.pbFeedUpdate.set_fraction(1.0)
self.pbFeedUpdate.set_text(_('No new episodes'))
self.feed_cache_update_cancelled = True
self.btnCancelFeedUpdate.show()
self.btnCancelFeedUpdate.set_sensitive(True)
self.itemUpdate.set_sensitive(True)
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
else:
count = len(episodes)
# New episodes are available
self.pbFeedUpdate.set_fraction(1.0)
# Are we minimized and should we auto download?
if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
self.download_episode_list(episodes)
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
elif self.config.auto_download == 'queue':
self.download_episode_list_paused(episodes)
title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
else:
self.show_update_feeds_buttons()
# New episodes are available and we are not minimized
if not self.config.do_not_show_new_episodes_dialog:
self.new_episodes_show(episodes, notification=True)
else:
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
self.pbFeedUpdate.set_text(message)
def _update_cover(self, channel):
if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
self.cover_downloader.request_cover(channel)
def update_feed_cache_proc(self, channels, select_url_afterwards):
total = len(channels)
for updated, channel in enumerate(channels):
if not self.feed_cache_update_cancelled:
try:
channel.update(max_episodes=self.config.max_episodes_per_feed, \
mimetype_prefs=self.config.mimetype_prefs)
self._update_cover(channel)
except Exception, e:
d = {'url': cgi.escape(channel.url), 'message': cgi.escape(str(e))}
if d['message']:
message = _('Error while updating %(url)s: %(message)s')
else:
message = _('The feed at %(url)s could not be updated.')
self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
logger.error('Error: %s', str(e), exc_info=True)
if self.feed_cache_update_cancelled:
break
# By the time we get here the update may have already been cancelled
if not self.feed_cache_update_cancelled:
def update_progress():
d = {'podcast': channel.title, 'position': updated+1, 'total': total}
progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
self.pbFeedUpdate.set_text(progression)
self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
util.idle_add(update_progress)
updated_urls = [c.url for c in channels]
util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
def show_update_feeds_buttons(self):
# Make sure that the buttons for updating feeds
# appear - this should happen after a feed update
@ -2358,20 +2268,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
else:
self.show_update_feeds_buttons()
def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
if self.updating_feed_cache:
return
if not force_update:
self.channels = Model.get_podcasts(self.db)
self.channel_list_changed = True
self.update_podcast_list_model(select_url=select_url_afterwards)
return
def update_feed_cache(self, channels=None):
# Fix URLs if mygpo has rewritten them
self.rewrite_urls_mygpo()
self.updating_feed_cache = True
# XXX somewhere else? self.rewrite_urls_mygpo()
if channels is None:
# Only update podcasts for which updates are enabled
@ -2387,16 +2286,103 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.hboxUpdateFeeds.show_all()
self.btnUpdateFeeds.hide()
if len(channels) == 1:
text = _('Updating "%s"...') % channels[0].title
else:
count = len(channels)
text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
count = len(channels)
text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
self.pbFeedUpdate.set_text(text)
self.pbFeedUpdate.set_fraction(0)
args = (channels, select_url_afterwards)
threading.Thread(target=self.update_feed_cache_proc, args=args).start()
def update_feed_cache_proc():
updated_channels = []
for updated, channel in enumerate(channels):
if self.feed_cache_update_cancelled:
break
try:
channel.update(max_episodes=self.config.max_episodes_per_feed,
mimetype_prefs=self.config.mimetype_prefs)
self._update_cover(channel)
except Exception, e:
d = {'url': cgi.escape(channel.url), 'message': cgi.escape(str(e))}
if d['message']:
message = _('Error while updating %(url)s: %(message)s')
else:
message = _('The feed at %(url)s could not be updated.')
self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
logger.error('Error: %s', str(e), exc_info=True)
updated_channels.append(channel)
def update_progress(channel):
self.update_podcast_list_model([channel.url])
# If the currently-viewed podcast is updated, reload episodes
if self.active_channel is not None and \
self.active_channel == channel:
logger.debug('Updated channel is active, updating UI')
self.update_episode_list_model()
d = {'podcast': channel.title, 'position': updated+1, 'total': count}
progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
self.pbFeedUpdate.set_text(progression)
self.pbFeedUpdate.set_fraction(float(updated+1)/float(count))
util.idle_add(update_progress, channel)
def update_feed_cache_finish_callback():
# Process received episode actions for all updated URLs
# XXX somewhere else? self.process_received_episode_actions(updated_urls)
# If we are currently viewing "All episodes", update its episode list now
if self.active_channel is not None and \
getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
self.update_episode_list_model()
if self.feed_cache_update_cancelled:
# The user decided to abort the feed update
self.show_update_feeds_buttons()
# Only search for new episodes in podcasts that have been
# updated, not in other podcasts (for single-feed updates)
episodes = self.get_new_episodes([c for c in updated_channels])
if not episodes:
# Nothing new here - but inform the user
self.pbFeedUpdate.set_fraction(1.0)
self.pbFeedUpdate.set_text(_('No new episodes'))
self.feed_cache_update_cancelled = True
self.btnCancelFeedUpdate.show()
self.btnCancelFeedUpdate.set_sensitive(True)
self.itemUpdate.set_sensitive(True)
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
else:
count = len(episodes)
# New episodes are available
self.pbFeedUpdate.set_fraction(1.0)
# Are we minimized and should we auto download?
if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
self.download_episode_list(episodes)
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
elif self.config.auto_download == 'queue':
self.download_episode_list_paused(episodes)
title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
self.show_update_feeds_buttons()
else:
self.show_update_feeds_buttons()
# New episodes are available and we are not minimized
if not self.config.do_not_show_new_episodes_dialog:
self.new_episodes_show(episodes, notification=True)
else:
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
self.pbFeedUpdate.set_text(message)
util.idle_add(update_feed_cache_finish_callback)
threading.Thread(target=update_feed_cache_proc).start()
def on_gPodder_delete_event(self, widget, *args):
"""Called when the GUI wants to close the window
@ -2640,8 +2626,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
def on_itemUpdate_activate(self, widget=None):
# Check if we have outstanding subscribe/unsubscribe actions
if self.on_add_remove_podcasts_mygpo():
return
# FIXME: Implement this somewhere else: self.on_add_remove_podcasts_mygpo()
if self.channels:
self.update_feed_cache()
@ -2753,11 +2738,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
_('No new episodes available'), widget=self.btnUpdateFeeds)
def get_new_episodes(self, channels=None):
is_new = lambda e: (e.state == gpodder.STATE_NORMAL and e.is_new and
not e.downloading)
return [e for c in channels or self.channels for e in
filter(is_new, c.get_all_episodes())]
filter(lambda e: e.check_is_new(), c.get_all_episodes())]
def commit_changes_to_database(self):
"""This will be called after the sync process is finished"""
@ -2887,7 +2869,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.mygpo_client.on_unsubscribe([c.url for c in channels])
# Re-load the channels and select the desired new channel
self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
self.update_podcast_list_model(select_url=select_url)
progress.on_finished()
def thread_proc():
@ -2929,8 +2911,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
# Clean up downloads and download directories
self.clean_up_downloads()
self.channel_list_changed = True
# The remaining stuff is to be done in the GTK main thread
util.idle_add(finish_deletion, select_url)
@ -3200,7 +3180,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
def _on_auto_update_timer(self):
logger.debug('Auto update timer fired.')
self.update_feed_cache(force_update=True)
self.update_feed_cache()
# Ask web service for sub changes (if enabled)
self.mygpo_client.flush()

View File

@ -323,6 +323,10 @@ class PodcastEpisode(PodcastModelObject):
return task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED)
def check_is_new(self):
return (self.state == gpodder.STATE_NORMAL and self.is_new and
not self.downloading)
def save(self):
if gpodder.user_hooks is not None:
gpodder.user_hooks.on_episode_save(self)
@ -645,10 +649,11 @@ class PodcastEpisode(PodcastModelObject):
class PodcastChannel(PodcastModelObject):
__slots__ = schema.PodcastColumns
UNICODE_TRANSLATE = {ord(u'ö'): u'o', ord(u'ä'): u'a', ord(u'ü'): u'u'}
MAX_FOLDERNAME_LENGTH = 150
SECONDS_PER_WEEK = 7*24*60*60
EpisodeClass = PodcastEpisode
@ -763,9 +768,14 @@ class PodcastChannel(PodcastModelObject):
return count
@classmethod
def sort_key(cls, podcast):
key = podcast.title.decode('utf-8', 'ignore').lower()
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
@classmethod
def load_from_db(cls, db):
return db.load_podcasts(cls.create_from_dict)
return sorted(db.load_podcasts(cls.create_from_dict), key=cls.sort_key)
@classmethod
def load(cls, db, url, create=True, authentication_tokens=None,\
@ -774,7 +784,7 @@ class PodcastChannel(PodcastModelObject):
if isinstance(url, unicode):
url = url.encode('utf-8')
existing = filter(lambda p: p.url == url, self.load_from_db(db))
existing = filter(lambda p: p.url == url, cls.load_from_db(db))
if existing:
return existing[0]
@ -1171,6 +1181,10 @@ class Model(object):
return cls.PodcastClass.load(db, url, create, authentication_tokens, \
max_episodes, mimetype_prefs)
@classmethod
def podcast_sort_key(cls, podcast):
return cls.PodcastClass.sort_key(podcast)
@staticmethod
def sort_episodes_by_pubdate(episodes, reverse=False):
"""Sort a list of PodcastEpisode objects chronologically