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:
parent
e3f5360073
commit
236ee1f6a7
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue