Download strategy: Only keep latest (bug 188)

Add per-podcast option to only keep the latest episode of a
channel (default strategy is still the current setting).
This commit is contained in:
Thomas Perl 2012-10-13 16:21:25 +02:00
parent e3f5360073
commit 236ee1f6a7
6 changed files with 156 additions and 61 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 2.24 -->
<requires lib="gtk+" version="2.24"/>
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkDialog" id="gPodderChannel">
<property name="can_focus">False</property>
@ -58,7 +58,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">10</property>
<property name="n_rows">5</property>
<property name="n_rows">6</property>
<property name="n_columns">4</property>
<property name="column_spacing">5</property>
<property name="row_spacing">10</property>
@ -75,7 +75,7 @@
<property name="right_attach">4</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -90,7 +90,7 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">4</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -105,7 +105,7 @@
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -126,8 +126,8 @@
</object>
<packing>
<property name="right_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
</packing>
</child>
<child>
@ -202,6 +202,37 @@
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_strategy">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Strategy:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="combo_strategy">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="tab">
@ -251,7 +282,7 @@
</object>
<packing>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -265,7 +296,7 @@
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -281,7 +312,7 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -299,7 +330,7 @@
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
</object>
@ -351,7 +382,7 @@
</object>
<packing>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -366,7 +397,7 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">3</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -380,7 +411,7 @@
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -397,7 +428,7 @@
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>

View File

@ -93,3 +93,43 @@ def find_partial_downloads(channels, start_progress_callback, progress_callback,
else:
clean_up_downloads(True)
def get_expired_episodes(channels, config):
for channel in channels:
for index, episode in enumerate(channel.get_downloaded_episodes()):
# Never consider archived episodes as old
if episode.archive:
continue
# Download strategy "Only keep latest"
if (channel.download_strategy == channel.STRATEGY_LATEST and
index > 0):
logger.info('Removing episode (only keep latest strategy): %s',
episode.title)
yield episode
continue
# Only expire episodes if the age in days is positive
if config.episode_old_age < 1:
continue
# Never consider fresh episodes as old
if episode.age_in_days() < config.episode_old_age:
continue
# Do not delete played episodes (except if configured)
if not episode.is_new:
if not config.auto_remove_played_episodes:
continue
# Do not delete unfinished episodes (except if configured)
if not episode.is_finished():
if not config.auto_remove_unfinished_episodes:
continue
# Do not delete unplayed episodes (except if configured)
if episode.is_new:
if not config.auto_remove_unplayed_episodes:
continue
yield episode

View File

@ -50,6 +50,19 @@ class gPodderChannel(BuilderWidget):
self.combo_section.add_attribute(cell_renderer, 'text', 0)
self.combo_section.set_active(active_index)
self.strategy_list = gtk.ListStore(str, int)
active_index = 0
for index, (checked, strategy_id, strategy) in \
enumerate(self.channel.get_download_strategies()):
self.strategy_list.append([strategy, strategy_id])
if checked:
active_index = index
self.combo_strategy.set_model(self.strategy_list)
cell_renderer = gtk.CellRendererText()
self.combo_strategy.pack_start(cell_renderer)
self.combo_strategy.add_attribute(cell_renderer, 'text', 0)
self.combo_strategy.set_active(active_index)
self.LabelDownloadTo.set_text( self.channel.save_dir)
self.LabelWebsite.set_text( self.channel.link)
@ -187,6 +200,9 @@ class gPodderChannel(BuilderWidget):
else:
section_changed = False
new_strategy = self.strategy_list[self.combo_strategy.get_active()][1]
self.channel.set_download_strategy(new_strategy)
self.channel.save()
self.gPodderChannel.destroy()

View File

@ -237,7 +237,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.restart_auto_update_timer()
# Find expired (old) episodes and delete them
old_episodes = list(self.get_expired_episodes())
old_episodes = list(common.get_expired_episodes(self.channels, self.config))
if len(old_episodes) > 0:
self.delete_episode_list(old_episodes, confirm=False)
updated_urls = set(e.channel.url for e in old_episodes)
@ -2509,40 +2509,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
if macapp is None:
sys.exit(0)
def get_expired_episodes(self):
# XXX: Move out of gtkui and into a generic module (gpodder.model)?
# Only expire episodes if the age in days is positive
if self.config.episode_old_age < 1:
return
for channel in self.channels:
for episode in channel.get_downloaded_episodes():
# Never consider archived episodes as old
if episode.archive:
continue
# Never consider fresh episodes as old
if episode.age_in_days() < self.config.episode_old_age:
continue
# Do not delete played episodes (except if configured)
if not episode.is_new:
if not self.config.auto_remove_played_episodes:
continue
# Do not delete unfinished episodes (except if configured)
if not episode.is_finished():
if not self.config.auto_remove_unfinished_episodes:
continue
# Do not delete unplayed episodes (except if configured)
if episode.is_new:
if not self.config.auto_remove_unplayed_episodes:
continue
yield episode
def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
if not episodes:
return False

View File

@ -791,6 +791,15 @@ class PodcastChannel(PodcastModelObject):
UNICODE_TRANSLATE = {ord(u'ö'): u'o', ord(u'ä'): u'a', ord(u'ü'): u'u'}
# Enumerations for download strategy
STRATEGY_DEFAULT, STRATEGY_LATEST = range(2)
# Description and ordering of strategies
STRATEGIES = [
(STRATEGY_DEFAULT, _('Default')),
(STRATEGY_LATEST, _('Only keep latest')),
]
MAX_FOLDERNAME_LENGTH = 60
SECONDS_PER_WEEK = 7*24*60*60
EpisodeClass = PodcastEpisode
@ -821,6 +830,7 @@ class PodcastChannel(PodcastModelObject):
self.section = _('Other')
self._common_prefix = None
self.download_strategy = PodcastChannel.STRATEGY_DEFAULT
@property
def model(self):
@ -830,6 +840,21 @@ class PodcastChannel(PodcastModelObject):
def db(self):
return self.parent.db
def get_download_strategies(self):
for value, caption in PodcastChannel.STRATEGIES:
yield self.download_strategy == value, value, caption
def set_download_strategy(self, download_strategy):
if download_strategy == self.download_strategy:
return
caption = dict(self.STRATEGIES).get(download_strategy)
if caption is not None:
logger.debug('Strategy for %s changed to %s', self.title, caption)
self.download_strategy = download_strategy
else:
logger.warn('Cannot set strategy to %d', download_strategy)
def check_download_folder(self):
"""Check the download folder for externally-downloaded files
@ -1037,21 +1062,21 @@ class PodcastChannel(PodcastModelObject):
# Load all episodes to update them properly.
existing = self.get_all_episodes()
# We can limit the maximum number of entries that gPodder will parse
if max_episodes > 0 and len(feed.entries) > max_episodes:
try:
# We have to sort the entries in descending chronological order,
# because if the feed lists items in ascending order and has >
# max_episodes old episodes, new episodes will not be shown.
# See also: gPodder Bug 1186
try:
entries = sorted(feed.entries, key=feedcore.get_pubdate,
reverse=True)[:max_episodes]
except Exception, e:
logger.warn('Could not sort episodes: %s', e, exc_info=True)
entries = feed.entries[:max_episodes]
else:
entries = sorted(feed.entries, key=feedcore.get_pubdate,
reverse=True)
except Exception, e:
logger.warn('Could not sort episodes: %s', e, exc_info=True)
entries = feed.entries
# We can limit the maximum number of entries that gPodder will parse
if max_episodes > 0 and len(entries) > max_episodes:
entries = entries[:max_episodes]
# Title + PubDate hashes for existing episodes
existing_dupes = dict((e.duplicate_id(), e) for e in existing)
@ -1064,6 +1089,9 @@ class PodcastChannel(PodcastModelObject):
# Keep track of episode GUIDs currently seen in the feed
seen_guids = set()
# Number of new episodes found
new_episodes = 0
# Search all entries for new episodes
for entry in entries:
try:
@ -1104,6 +1132,12 @@ class PodcastChannel(PodcastModelObject):
existing_episode.save()
continue
new_episodes += 1
# Only allow a certain number of new episodes per update
if (self.download_strategy == PodcastChannel.STRATEGY_LATEST and
new_episodes > 1):
episode.is_new = False
# Workaround for bug 340: If the episode has been
# published earlier than one week before the most
# recent existing episode, do not mark it as new.

View File

@ -61,9 +61,10 @@ PodcastColumns = (
'pause_subscription',
'section',
'payment_url',
'download_strategy',
)
CURRENT_VERSION = 3
CURRENT_VERSION = 4
# SQL commands to upgrade old database versions to new ones
@ -82,6 +83,11 @@ UPGRADE_SQL = [
ALTER TABLE episode ADD COLUMN payment_url TEXT NULL DEFAULT NULL
UPDATE podcast SET http_last_modified=NULL, http_etag=NULL
"""),
# Version 4: Per-podcast download strategy management
(3, 4, """
ALTER TABLE podcast ADD COLUMN download_strategy INTEGER NOT NULL DEFAULT 0
""")
]
def initialize_database(db):
@ -102,7 +108,8 @@ def initialize_database(db):
download_folder TEXT NOT NULL DEFAULT '',
pause_subscription INTEGER NOT NULL DEFAULT 0,
section TEXT NOT NULL DEFAULT '',
payment_url TEXT NULL DEFAULT NULL
payment_url TEXT NULL DEFAULT NULL,
download_strategy INTEGER NOT NULL DEFAULT 0
)
""")
@ -224,6 +231,7 @@ def convert_gpodder2_db(old_db, new_db):
not row['feed_update_enabled'],
'',
None,
0,
)
new_db.execute("""
INSERT INTO podcast VALUES (%s)