Compare commits
3 commits
d3e432e165
...
3577fca9ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 3577fca9ba | |||
| da60198bbb | |||
| 371c9b9222 |
8 changed files with 117 additions and 9 deletions
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
16
podcasts/migrations/0013_feed_notify.py
Normal file
16
podcasts/migrations/0013_feed_notify.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,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,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1056,7 +1057,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),
|
||||
|
|
@ -1082,6 +1083,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):
|
||||
|
|
@ -1116,6 +1118,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()
|
||||
|
|
@ -1144,12 +1157,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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -918,6 +919,7 @@ FEED_SETTING_PARSERS = {
|
|||
'exclude_words': _word_list,
|
||||
'min_duration': lambda v: _whole(v, 0, 600),
|
||||
'auto_download': _boolean,
|
||||
'notify': _boolean,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v63';
|
||||
const CACHE = 'diora-v64';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app.js',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android 3d1281c 2026-10-02: web-native/static/native-podcast.js
|
||||
// Copied from diora-android 6b0855e 2026-10-02: web-native/static/native-podcast.js
|
||||
// by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android
|
||||
// and copy again. The browser host is static/js/podcasts-web.js.
|
||||
|
||||
|
|
@ -274,6 +274,7 @@
|
|||
action: () => N.podcastSettings(JSON.stringify({auto_download_default: !s.auto_download_default}))},
|
||||
...dlSettingsItems(),
|
||||
]),
|
||||
backgroundSyncItem(),
|
||||
group('⇅', 'Abos umziehen', 'OPML exportieren und importieren', () => [
|
||||
{icon: '⇩', label: 'Abos exportieren (OPML)', hint: 'zum Umziehen in eine andere App', action: exportOpml},
|
||||
{icon: '⇧', label: 'Abos importieren (OPML)', action: importOpml},
|
||||
|
|
@ -282,6 +283,25 @@
|
|||
]);
|
||||
}
|
||||
|
||||
// The app's background job (BackgroundSync.kt): new episodes, their notifications and downloads with the app closed.
|
||||
const BG_SYNC = [
|
||||
{value: 0, label: 'Aus', hint: 'nur wenn die App offen ist'},
|
||||
{value: 15, label: 'Alle 15 Minuten'},
|
||||
{value: 60, label: 'Jede Stunde', hint: 'Standard'},
|
||||
{value: 240, label: 'Alle 4 Stunden'},
|
||||
];
|
||||
|
||||
function backgroundSyncItem() {
|
||||
if (!N.podcastBackgroundSync || !N.podcastSetBackgroundSync) return null;
|
||||
const now = N.podcastBackgroundSync();
|
||||
const label = (BG_SYNC.find(c => c.value === now) || BG_SYNC[2]).label;
|
||||
return {icon: '⟲', label: 'Im Hintergrund abgleichen', hint: label + ' · neue Folgen, Mitteilungen, Downloads',
|
||||
action: () => openSheet('Im Hintergrund abgleichen', BG_SYNC.map(c => ({
|
||||
icon: c.value === now ? '●' : '○', label: c.label, hint: c.hint, active: c.value === now,
|
||||
action: () => { N.podcastSetBackgroundSync(c.value); settingsMenu(); },
|
||||
})))};
|
||||
}
|
||||
|
||||
function skipItem(name, label, choices) {
|
||||
if (!N.podcastSetPrefValue) return null;
|
||||
const now = devicePrefs()[name];
|
||||
|
|
@ -1078,6 +1098,15 @@
|
|||
{icon: !live.auto_download ? '●' : '○', label: 'Nein', hint: 'nur die Warteschlange wird geladen', action: () => save({auto_download: false})},
|
||||
])));
|
||||
}
|
||||
// AntennaPod's episode notifications: the phone tells when a new episode is in „Neu“.
|
||||
if ('notify' in live) {
|
||||
row.append(chip('Benachrichtigen', live.notify ? 'an' : 'aus', !!live.notify, () =>
|
||||
openSheet(`Bei neuen Folgen von „${plain(live.title)}“ benachrichtigen?`, [
|
||||
{icon: live.notify ? '●' : '○', label: 'Ja', hint: 'eine Mitteilung auf dem Telefon, sobald eine neue Folge in „Neu“ ist',
|
||||
action: () => save({notify: true})},
|
||||
{icon: !live.notify ? '●' : '○', label: 'Nein', action: () => save({notify: false})},
|
||||
])));
|
||||
}
|
||||
row.append(chip('Filter', words || 'aus', !!words, () => openSheet('Welche neuen Folgen zählen?', [
|
||||
{icon: '+', label: 'Nur mit diesen Wörtern im Titel', hint: live.include_words || 'alle',
|
||||
action: () => ask('include_words', 'Nur Folgen, deren Titel eines dieser Wörter enthält (mit Komma getrennt):', live.include_words)},
|
||||
|
|
@ -3101,6 +3130,26 @@
|
|||
render();
|
||||
};
|
||||
|
||||
// A new-episode notification was tapped: that podcast's page (gone meanwhile: „Neu“).
|
||||
window.__dioraOpenFeed = function (feedId) {
|
||||
if (typeof showTab !== 'function') return;
|
||||
closeStats();
|
||||
closeDownloads();
|
||||
closeNotes();
|
||||
closePlayer();
|
||||
selected = null;
|
||||
showTab('podcasts');
|
||||
const f = feeds().find(x => x.id === feedId);
|
||||
if (f) {
|
||||
openFeed(f);
|
||||
} else {
|
||||
feedView = null;
|
||||
view = 'inbox';
|
||||
render();
|
||||
scrollTop(0);
|
||||
}
|
||||
};
|
||||
|
||||
window.__dioraPodcastsRefreshed = function (ok) {
|
||||
refreshing = false;
|
||||
if (!ok) toast(navigator.onLine ? 'Abrufen hat nicht geklappt' : 'Offline — es gilt der letzte Stand');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue