Podcast-Einstellung "notify": über neue Folgen benachrichtigen

PodcastFeed.notify (Standard aus) wie auto_download: in feeds/ und
state/, änderbar über feeds/<id>/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 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-02 15:45:51 +02:00
parent 262dd0d207
commit 371c9b9222
6 changed files with 66 additions and 7 deletions

View file

@ -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',
}

View file

@ -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),
),
]

View file

@ -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')

View file

@ -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)

View file

@ -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):

View file

@ -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,
}