Podcasts: neues Abo bringt nur die neueste Folge in die Inbox
All checks were successful
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 2m3s

Beim ersten Abruf eines Feeds (Abonnieren, OPML-Import, gpodder) kam der
ganze Rückkatalog als „neu“ in die Inbox (bei Lage der Nation 200
Folgen). Jetzt zählt nur die neueste; die älteren sind ausgeblendet,
nicht gehört, und stehen weiter in der Folgenliste des Podcasts. Auch
automatisches Einreihen nimmt beim ersten Abruf nur diese eine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-02 07:48:45 +02:00
parent 176771a407
commit a1e17081c5
2 changed files with 48 additions and 0 deletions

View file

@ -111,6 +111,7 @@ class AutoQueueTests(TestCase):
self.user = User.objects.create_user(username='alice', password='pw12345678') self.user = User.objects.create_user(username='alice', password='pw12345678')
self.feed = PodcastFeed.objects.create( self.feed = PodcastFeed.objects.create(
user=self.user, rss_url='https://example.org/feed', title='Feed', auto_queue=True, user=self.user, rss_url='https://example.org/feed', title='Feed', auto_queue=True,
last_refreshed_at=datetime(2026, 1, 1, tzinfo=timezone.utc), # not the first fetch
) )
other = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/o', title='O') other = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/o', title='O')
self.old = [ self.old = [
@ -148,6 +149,39 @@ class AutoQueueTests(TestCase):
self.assertEqual(self.refresh(), ['o0', 'o1', 'o2']) self.assertEqual(self.refresh(), ['o0', 'o1', 'o2'])
class FirstFetchTests(TestCase):
"""A new subscription doesn't flood the inbox with its back catalogue."""
def setUp(self):
self.user = User.objects.create_user(username='alice', password='pw12345678')
self.client.force_login(self.user)
self.rss = _rss([(f'e{i}', f'Mon, {i + 1:02d} Jan 2026 10:00:00 GMT') for i in range(5)])
def subscribe(self):
with patch('podcasts.views._fetch_feed', lambda url: _parse(self.rss)):
return self.client.post('/podcasts/feeds/add/', json.dumps({'rss_url': 'https://example.org/f'}),
content_type='application/json')
def test_only_the_newest_is_new(self):
self.assertEqual(self.subscribe().json()['new_episodes'], 5)
inbox = self.client.get('/podcasts/inbox/').json()['episodes']
self.assertEqual([e['title'] for e in inbox], ['e4'])
# The others are still listed for the podcast, not marked played.
feed = PodcastFeed.objects.get(user=self.user)
eps = self.client.get(f'/podcasts/feeds/{feed.id}/episodes/').json()['episodes']
self.assertEqual(len(eps), 5)
self.assertFalse(any(e['played'] for e in eps))
def test_later_fetches_keep_every_new_episode(self):
self.subscribe()
feed = PodcastFeed.objects.get(user=self.user)
self.rss = _rss([(f'e{i}', f'Mon, {i + 1:02d} Jan 2026 10:00:00 GMT') for i in range(8)])
with patch('podcasts.views._fetch_feed', lambda url: _parse(self.rss)):
_refresh_feed(feed)
inbox = self.client.get('/podcasts/inbox/').json()['episodes']
self.assertEqual([e['title'] for e in inbox], ['e7', 'e6', 'e5', 'e4'])
class RefreshAllTests(TestCase): class RefreshAllTests(TestCase):
"""One request refreshes all feeds on the server; one request returns the state.""" """One request refreshes all feeds on the server; one request returns the state."""

View file

@ -19,6 +19,7 @@ from .models import EpisodeProgress, PodcastEpisode, PodcastFeed, PodcastQueue,
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FEED_TIMEOUT = 20 # seconds per feed download FEED_TIMEOUT = 20 # seconds per feed download
NEW_ON_SUBSCRIBE = 1 # episodes of a new subscription that count as new
FEED_WORKERS = 8 # feeds downloaded at the same time FEED_WORKERS = 8 # feeds downloaded at the same time
@ -167,6 +168,8 @@ def _refresh_feed(feed_obj, parsed=None):
if parsed is None: if parsed is None:
parsed = _fetch_feed(feed_obj.rss_url) parsed = _fetch_feed(feed_obj.rss_url)
channel = parsed.feed channel = parsed.feed
# The first fetch of a new subscription brings the whole back catalogue.
first_fetch = feed_obj.last_refreshed_at is None
feed_obj.title = channel.get('title', feed_obj.title or feed_obj.rss_url)[:300] feed_obj.title = channel.get('title', feed_obj.title or feed_obj.rss_url)[:300]
feed_obj.description = channel.get('subtitle', channel.get('description', ''))[:2000] feed_obj.description = channel.get('subtitle', channel.get('description', ''))[:2000]
@ -245,6 +248,17 @@ def _refresh_feed(feed_obj, parsed=None):
new_count += 1 new_count += 1
new_episodes_list.append(episode_obj) new_episodes_list.append(episode_obj)
if first_fetch and len(new_episodes_list) > NEW_ON_SUBSCRIBE:
# Only the newest episodes count as new; the rest stay in the
# podcast's episode list, out of the inbox (dismissed, not played).
newest_first = sorted(new_episodes_list, key=lambda e: e.pub_date or e.discovered_at, reverse=True)
older = newest_first[NEW_ON_SUBSCRIBE:]
EpisodeProgress.objects.bulk_create(
[EpisodeProgress(user=feed_obj.user, episode=e, dismissed=True) for e in older],
ignore_conflicts=True,
)
new_episodes_list = newest_first[:NEW_ON_SUBSCRIBE]
if feed_obj.auto_queue and new_episodes_list: if feed_obj.auto_queue and new_episodes_list:
where = PodcastSettings.for_user(feed_obj.user).auto_queue_position where = PodcastSettings.for_user(feed_obj.user).auto_queue_position
# Newest first in front of what's there; oldest first when appended, # Newest first in front of what's there; oldest first when appended,