From 371c9b9222bcc1c2084707186f79145ef9580b5a Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Fri, 2 Oct 2026 15:45:51 +0200 Subject: [PATCH] =?UTF-8?q?Podcast-Einstellung=20"notify":=20=C3=BCber=20n?= =?UTF-8?q?eue=20Folgen=20benachrichtigen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PodcastFeed.notify (Standard aus) wie auto_download: in feeds/ und state/, änderbar über feeds//settings/ (nur true/false). Die Geräte melden damit neue Folgen dieses Podcasts. Der AntennaPod-Import übernimmt Feeds.episode_notification (an -> notify an; aus, AntennaPods Standard, lässt dioras Einstellung stehen; ältere Backups ohne die Spalte: aus). Co-Authored-By: Claude Opus 5.5 --- podcasts/antennapod.py | 10 +++++++--- podcasts/migrations/0013_feed_notify.py | 16 ++++++++++++++++ podcasts/models.py | 3 +++ podcasts/test_antennapod.py | 23 ++++++++++++++++++++++- podcasts/tests.py | 19 ++++++++++++++++--- podcasts/views.py | 2 ++ 6 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 podcasts/migrations/0013_feed_notify.py diff --git a/podcasts/antennapod.py b/podcasts/antennapod.py index 456a92e..357d631 100644 --- a/podcasts/antennapod.py +++ b/podcasts/antennapod.py @@ -43,7 +43,7 @@ SUBSCRIBED = 0 # Feeds.state MAX_FILESIZE = 4_000_000_000 SETTING_FIELDS = ('playback_speed', 'skip_intro', 'skip_outro', 'volume', 'include_words', - 'exclude_words', 'min_duration', 'new_episodes', 'auto_download') + 'exclude_words', 'min_duration', 'new_episodes', 'auto_download', 'notify') # Columns read from the backup, with the value to assume if an older # AntennaPod didn't have the column yet. @@ -52,7 +52,7 @@ FEED_COLUMNS = { 'image_url': '', 'keep_updated': 1, 'include_filter': '', 'exclude_filter': '', 'minimal_duration_filter': -1, 'new_episodes_action': 0, 'feed_playback_speed': -1, 'feed_skip_intro': 0, 'feed_skip_ending': 0, 'feed_volume_adaption': 0, 'auto_download': 1, - 'state': SUBSCRIBED, + 'episode_notification': 0, 'state': SUBSCRIBED, } @@ -191,6 +191,10 @@ def feed_settings(ap, global_auto_download=False): s['auto_download'] = bool(global_auto_download) elif code == AUTO_DOWNLOAD_OFF: s['auto_download'] = False + # Feeds.episode_notification: 1 = a notification for each new episode + # (0, AntennaPod's default, leaves diora's setting alone). + if ap['episode_notification']: + s['notify'] = True return s, not_kept_updated @@ -769,7 +773,7 @@ class AntennaPodImport: SETTING_LABELS = { 'playback_speed': 'speed', 'skip_intro': 'skip intro', 'skip_outro': 'skip outro', 'volume': 'volume', 'include_words': 'include words', 'exclude_words': 'exclude words', 'min_duration': 'min duration', - 'new_episodes': 'new episodes', 'auto_download': 'auto-download', + 'new_episodes': 'new episodes', 'auto_download': 'auto-download', 'notify': 'notifications', } diff --git a/podcasts/migrations/0013_feed_notify.py b/podcasts/migrations/0013_feed_notify.py new file mode 100644 index 0000000..b1df205 --- /dev/null +++ b/podcasts/migrations/0013_feed_notify.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('podcasts', '0012_player_state'), + ] + + operations = [ + migrations.AddField( + model_name='podcastfeed', + name='notify', + field=models.BooleanField(default=False), + ), + ] diff --git a/podcasts/models.py b/podcasts/models.py index 54c8a38..30f4c85 100644 --- a/podcasts/models.py +++ b/podcasts/models.py @@ -51,6 +51,9 @@ class PodcastFeed(models.Model): # Devices download this feed's new episodes (those in the inbox) on # their own, for listening offline. auto_download = models.BooleanField(default=False) + # Devices tell about this feed's new episodes (a notification, like + # AntennaPod's per-podcast "episode notifications"). + notify = models.BooleanField(default=False) class Meta: unique_together = ('user', 'rss_url') diff --git a/podcasts/test_antennapod.py b/podcasts/test_antennapod.py index 91207c1..2fe86b6 100644 --- a/podcasts/test_antennapod.py +++ b/podcasts/test_antennapod.py @@ -31,7 +31,8 @@ class Backup: minimal_duration_filter INTEGER DEFAULT -1, new_episodes_action INTEGER DEFAULT 0, feed_playback_speed REAL DEFAULT -1, feed_skip_intro INTEGER DEFAULT 0, feed_skip_ending INTEGER DEFAULT 0, feed_volume_adaption INTEGER DEFAULT 0, - auto_download INTEGER DEFAULT 0, state INTEGER DEFAULT 0, tags TEXT); + auto_download INTEGER DEFAULT 0, episode_notification INTEGER DEFAULT 0, state INTEGER DEFAULT 0, + tags TEXT); CREATE TABLE FeedItems (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, pubDate INTEGER, read INTEGER, link TEXT, description TEXT, media INTEGER, feed INTEGER, item_identifier TEXT, image_url TEXT); @@ -253,6 +254,26 @@ class FeedImportTests(ImportTestCase): self.assertIn('default for new subscriptions: on\n', self.run_import('--global-auto-download', 'on')) self.assertIn('default for new subscriptions: on\n', self.run_import()) + def test_episode_notifications(self): + self.diora_feed('https://example.org/diora-on', notify=True) + self.diora_feed('https://example.org/on-both', notify=True) + self.ap.feed('https://example.org/diora-on', episode_notification=0) + self.ap.feed('https://example.org/on-both', episode_notification=1) + self.ap.feed('https://example.org/on', episode_notification=1) + self.ap.feed('https://example.org/off') + out = self.run_import() + values = dict(PodcastFeed.objects.filter(user=self.user).values_list('rss_url', 'notify')) + # AntennaPod's "off" is its default: diora's own setting stays. + self.assertEqual(values, {'https://example.org/diora-on': True, 'https://example.org/on-both': True, + 'https://example.org/on': True, 'https://example.org/off': False}) + self.assertIn('notifications 1', out) + + def test_backup_without_episode_notification_column(self): + self.ap.db.execute('ALTER TABLE Feeds DROP COLUMN episode_notification') + self.ap.feed('https://example.org/old') + self.run_import() + self.assertFalse(PodcastFeed.objects.get(user=self.user, rss_url='https://example.org/old').notify) + def test_created_feeds_start_with_the_users_default(self): PodcastSettings.objects.create(user=self.user, auto_download_default=True) self.ap.feed('https://example.org/explicit-off', auto_download=0) diff --git a/podcasts/tests.py b/podcasts/tests.py index d6186ad..22aea29 100644 --- a/podcasts/tests.py +++ b/podcasts/tests.py @@ -972,6 +972,7 @@ class OpmlExportTests(TestCase): FEED_DEFAULTS = { 'playback_speed': None, 'skip_intro': 0, 'skip_outro': 0, 'volume': 1.0, 'new_episodes': 'inbox', 'include_words': '', 'exclude_words': '', 'min_duration': 0, 'auto_queue': False, 'auto_download': False, + 'notify': False, } @@ -997,7 +998,7 @@ class FeedSettingsTests(TestCase): self.assertEqual(r.json(), { 'feed_id': self.feed.id, 'playback_speed': 1.5, 'skip_intro': 30, 'skip_outro': 600, 'volume': 2.0, 'new_episodes': 'inbox', 'include_words': 'News, Talk', 'exclude_words': 'Teaser', 'min_duration': 10, - 'auto_queue': False, 'auto_download': False, + 'auto_queue': False, 'auto_download': False, 'notify': False, }) self.feed.refresh_from_db() self.assertEqual((self.feed.playback_speed, self.feed.skip_intro, self.feed.volume, self.feed.min_duration), @@ -1023,6 +1024,7 @@ class FeedSettingsTests(TestCase): ('include_words', 'x' * 501), ('include_words', 5), ('exclude_words', ['a']), ('min_duration', -1), ('min_duration', 601), ('auto_download', 1), ('auto_download', 'true'), ('auto_download', None), + ('notify', 1), ('notify', 'false'), ('notify', None), ] for name, value in bad: with self.subTest(name=name, value=value): @@ -1057,6 +1059,17 @@ class FeedSettingsTests(TestCase): self.feed.refresh_from_db() self.assertFalse(self.feed.auto_download) + def test_notify_on_and_off(self): + self.assertIs(self.post({'notify': True}).json()['notify'], True) + self.feed.refresh_from_db() + self.assertTrue(self.feed.notify) + # Other settings (auto_download too) leave it alone. + self.assertIs(self.post({'auto_download': True}).json()['notify'], True) + self.assertIs(self.post({'notify': False}).json()['notify'], False) + self.feed.refresh_from_db() + self.assertFalse(self.feed.notify) + self.assertTrue(self.feed.auto_download) + def test_new_episodes_and_auto_queue_stay_in_step(self): self.assertEqual(self.post({'new_episodes': 'queue'}).json()['auto_queue'], True) self.feed.refresh_from_db() @@ -1085,12 +1098,12 @@ class FeedSettingsTests(TestCase): def test_feeds_and_state_carry_the_settings(self): self.post({'playback_speed': 1.25, 'skip_outro': 20, 'volume': 0.5, 'new_episodes': 'queue', - 'exclude_words': 'Trailer', 'min_duration': 5, 'auto_download': True}) + 'exclude_words': 'Trailer', 'min_duration': 5, 'auto_download': True, 'notify': True}) PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/2', title='Zweiter') expected = { self.feed.id: {'feed_id': self.feed.id, **FEED_DEFAULTS, 'playback_speed': 1.25, 'skip_outro': 20, 'volume': 0.5, 'new_episodes': 'queue', 'exclude_words': 'Trailer', 'min_duration': 5, - 'auto_queue': True, 'auto_download': True}, + 'auto_queue': True, 'auto_download': True, 'notify': True}, } for url in ('/podcasts/feeds/', '/podcasts/state/'): with self.subTest(url=url): diff --git a/podcasts/views.py b/podcasts/views.py index eb2f932..4f3ea6d 100644 --- a/podcasts/views.py +++ b/podcasts/views.py @@ -725,6 +725,7 @@ def feed_list(request): FEED_SETTING_FIELDS = ( 'playback_speed', 'skip_intro', 'skip_outro', 'volume', 'new_episodes', 'include_words', 'exclude_words', 'min_duration', 'auto_queue', 'auto_download', + 'notify', ) @@ -896,6 +897,7 @@ FEED_SETTING_PARSERS = { 'exclude_words': _word_list, 'min_duration': lambda v: _whole(v, 0, 600), 'auto_download': _boolean, + 'notify': _boolean, }