AntennaPod-Import: „Neu“ wie in AntennaPod
Folgen archivierter Podcasts (neue Folgen ignorieren), die AntennaPod nie abgerufen hat, verlassen „Neu“, sofern sie vor dem Backup erschienen sind; --quiet-diora-only macht das auch für Abos, die nur diora hat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
561776556f
commit
07aed26864
3 changed files with 108 additions and 3 deletions
|
|
@ -260,6 +260,8 @@ class Stats:
|
|||
downloads_created: int = 0
|
||||
downloads_missing: int = 0
|
||||
downloads_offered: bool = True
|
||||
ignored_left_inbox: int = 0
|
||||
diora_only_left_inbox: int = 0
|
||||
backup_date: datetime | None = None
|
||||
seconds: float = 0.0
|
||||
|
||||
|
|
@ -292,13 +294,17 @@ def _columns(db, table):
|
|||
|
||||
|
||||
class AntennaPodImport:
|
||||
def __init__(self, user, path, backup_date, global_auto_download=False, offer_downloads=True):
|
||||
def __init__(self, user, path, backup_date, global_auto_download=False, offer_downloads=True,
|
||||
quiet_diora_only=False):
|
||||
self.user = user
|
||||
self.path = path
|
||||
self.backup_date = backup_date
|
||||
self.global_auto_download = global_auto_download
|
||||
# False: the app doesn't offer AntennaPod's downloads (they stay in AntennaPod).
|
||||
self.offer_downloads = offer_downloads
|
||||
# True: subscriptions only diora has keep their episodes, but the
|
||||
# ones out before the backup leave the inbox.
|
||||
self.quiet_diora_only = quiet_diora_only
|
||||
self.stats = Stats(backup_date=backup_date)
|
||||
|
||||
def run(self, dry_run=False):
|
||||
|
|
@ -340,6 +346,7 @@ class AntennaPodImport:
|
|||
states, not_new, item_episode, downloads = self._import_episodes(
|
||||
db, feed_map, set(queue_items), favorite_items)
|
||||
self._import_progress(states, not_new)
|
||||
self._quiet_inbox(feed_map, states)
|
||||
self._import_queue(queue_items, item_episode)
|
||||
self._import_downloads(settings, downloads, item_episode)
|
||||
|
||||
|
|
@ -629,6 +636,55 @@ class AntennaPodImport:
|
|||
for ids in _chunks(dismiss_only):
|
||||
EpisodeProgress.objects.filter(pk__in=ids).update(dismissed=True)
|
||||
|
||||
def _quiet_inbox(self, feed_map, states):
|
||||
"""Episodes AntennaPod never showed as new, though diora has them in
|
||||
the inbox: those of podcasts whose new episodes are ignored (AntennaPod
|
||||
stopped fetching archived ones, so it never saw them), and with
|
||||
quiet_diora_only those of podcasts AntennaPod doesn't have. Only ones
|
||||
out before the backup; AntennaPod's own new ones stay."""
|
||||
st = self.stats
|
||||
user = self.user
|
||||
backup = self.backup_date
|
||||
imported = set(feed_map.values())
|
||||
feeds = set(PodcastFeed.objects.filter(pk__in=imported, new_episodes=PodcastFeed.NEW_IGNORE)
|
||||
.values_list('pk', flat=True))
|
||||
diora_only = set()
|
||||
if self.quiet_diora_only:
|
||||
diora_only = set(PodcastFeed.objects.filter(user=user).exclude(pk__in=imported).values_list('pk', flat=True))
|
||||
if not feeds and not diora_only:
|
||||
return
|
||||
keep = {ep_id for ep_id, s in states.items() if not s.dismissed}
|
||||
hidden = EpisodeProgress.objects.filter(user=user).filter(Q(played=True) | Q(dismissed=True))
|
||||
rows = (PodcastEpisode.objects.filter(feed_id__in=feeds | diora_only)
|
||||
.filter(Q(pub_date__lte=backup) | Q(pub_date__isnull=True))
|
||||
.exclude(id__in=hidden.values('episode_id'))
|
||||
.values_list('id', 'feed_id'))
|
||||
ids = []
|
||||
for ep_id, feed_id in rows:
|
||||
if ep_id in keep:
|
||||
continue
|
||||
ids.append(ep_id)
|
||||
if feed_id in feeds:
|
||||
st.ignored_left_inbox += 1
|
||||
else:
|
||||
st.diora_only_left_inbox += 1
|
||||
# Changed in diora after the backup: diora's state stays.
|
||||
newer = set()
|
||||
for chunk in _chunks(ids):
|
||||
newer.update(EpisodeProgress.objects.filter(user=user, episode_id__in=chunk, updated_at__gt=backup)
|
||||
.values_list('episode_id', flat=True))
|
||||
ids = [i for i in ids if i not in newer]
|
||||
for chunk in _chunks(ids):
|
||||
EpisodeProgress.objects.filter(user=user, episode_id__in=chunk, dismissed=False).update(dismissed=True)
|
||||
have = set(EpisodeProgress.objects.filter(user=user, episode_id__in=chunk)
|
||||
.values_list('episode_id', flat=True))
|
||||
with _given_updated_at():
|
||||
EpisodeProgress.objects.bulk_create(
|
||||
[EpisodeProgress(user=user, episode_id=i, dismissed=True, updated_at=backup)
|
||||
for i in chunk if i not in have],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# -- queue --------------------------------------------------------------
|
||||
|
||||
def _import_queue(self, queue_items, item_episode):
|
||||
|
|
@ -725,7 +781,9 @@ def summary_lines(st, username, path, dry_run, global_auto_download):
|
|||
f'Queue: {st.queue_backup} in backup ({st.queue_missing} not found); {st.queue_before} before, '
|
||||
f'{st.queue_after} after ({st.queue_appended} diora entries after AntennaPod\'s)'
|
||||
+ (f'; kept sorted by "{st.keep_sorted}"' if st.keep_sorted else ''),
|
||||
f'Inbox: {st.inbox_before} before, {st.inbox_after} after',
|
||||
f'Inbox: {st.inbox_before} before, {st.inbox_after} after; out of it: {st.ignored_left_inbox} of podcasts '
|
||||
f'whose new episodes are ignored (AntennaPod never showed them)'
|
||||
+ (f', {st.diora_only_left_inbox} of podcasts only diora has' if st.diora_only_left_inbox else ''),
|
||||
f'Downloads: {st.downloads} on the phone in AntennaPod ({st.downloads_bytes / 1_000_000:.0f} MB) '
|
||||
+ ('-> offered to the app for download' if st.downloads_offered else '-> not offered (--no-downloads)')
|
||||
+ (f' ({st.downloads_created} episodes created for them)' if st.downloads_created else '')
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ class Command(BaseCommand):
|
|||
help='Do everything inside a transaction that is rolled back, print the summary')
|
||||
parser.add_argument('--global-auto-download', choices=('on', 'off'), default='off',
|
||||
help="Auto-download for feeds that use AntennaPod's global setting (default off)")
|
||||
parser.add_argument('--quiet-diora-only', action='store_true',
|
||||
help='Podcasts only diora has (not in the backup): their episodes out before the backup '
|
||||
'leave the inbox (the subscriptions stay)')
|
||||
parser.add_argument('--no-downloads', action='store_true',
|
||||
help="Don't offer AntennaPod's downloaded episodes to the app (they stay in AntennaPod)")
|
||||
parser.add_argument('--backup-date',
|
||||
|
|
@ -53,7 +56,8 @@ class Command(BaseCommand):
|
|||
global_auto_download = options['global_auto_download'] == 'on'
|
||||
try:
|
||||
stats = AntennaPodImport(user, path, backup_date, global_auto_download,
|
||||
offer_downloads=not options['no_downloads']).run(dry_run=options['dry_run'])
|
||||
offer_downloads=not options['no_downloads'],
|
||||
quiet_diora_only=options['quiet_diora_only']).run(dry_run=options['dry_run'])
|
||||
except AntennaPodImportError as e:
|
||||
raise CommandError(str(e))
|
||||
for line in summary_lines(stats, user.username, path, options['dry_run'], global_auto_download):
|
||||
|
|
|
|||
|
|
@ -576,6 +576,49 @@ class ImportDownloadsViewTests(TestCase):
|
|||
self.assertEqual(len(PodcastSettings.objects.get(user=self.user).import_downloads), 4)
|
||||
|
||||
|
||||
class QuietInboxTests(ImportTestCase):
|
||||
"""What AntennaPod never showed as new leaves diora's inbox too."""
|
||||
|
||||
def inbox(self):
|
||||
hidden = EpisodeProgress.objects.filter(user=self.user, dismissed=True).values('episode_id')
|
||||
return sorted(PodcastEpisode.objects.filter(feed__user=self.user).exclude(id__in=hidden)
|
||||
.values_list('guid', flat=True))
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
archived = self.diora_feed('https://example.org/archived')
|
||||
self.ap_feed = self.ap.feed('https://example.org/archived', keep_updated=0)
|
||||
self.ap.item(self.ap_feed, 'ap-new', read=-1)
|
||||
self.episode(archived, 'ap-new', pub_date=T0)
|
||||
self.episode(archived, 'unknown-old', pub_date=T0 + timedelta(days=3)) # AntennaPod never fetched it
|
||||
self.episode(archived, 'unknown-late', pub_date=BACKUP_DATE + timedelta(days=1))
|
||||
self.episode(archived, 'undated')
|
||||
self.ap.feed('https://example.org/kept')
|
||||
kept = self.diora_feed('https://example.org/kept')
|
||||
self.episode(kept, 'kept-unknown', pub_date=T0) # not archived: stays
|
||||
only = self.diora_feed('https://example.org/only-diora')
|
||||
self.episode(only, 'only-old', pub_date=T0)
|
||||
self.episode(only, 'only-late', pub_date=BACKUP_DATE + timedelta(days=1))
|
||||
|
||||
def test_archived_podcasts(self):
|
||||
out = self.run_import()
|
||||
self.assertEqual(self.inbox(), ['ap-new', 'kept-unknown', 'only-late', 'only-old', 'unknown-late'])
|
||||
self.assertIn('out of it: 2 of podcasts whose new episodes are ignored (AntennaPod never showed them)\n', out)
|
||||
p = self.progress(PodcastEpisode.objects.get(guid='unknown-old'))
|
||||
self.assertEqual((p.played, p.dismissed, p.updated_at), (False, True, BACKUP_DATE))
|
||||
|
||||
def test_quiet_diora_only(self):
|
||||
out = self.run_import('--quiet-diora-only')
|
||||
self.assertEqual(self.inbox(), ['ap-new', 'kept-unknown', 'only-late', 'unknown-late'])
|
||||
self.assertIn(', 1 of podcasts only diora has', out)
|
||||
|
||||
def test_diora_newer_stays(self):
|
||||
ep = PodcastEpisode.objects.get(guid='unknown-old')
|
||||
EpisodeProgress.objects.create(user=self.user, episode=ep, position_seconds=30) # updated now
|
||||
self.run_import()
|
||||
self.assertIn('unknown-old', self.inbox())
|
||||
|
||||
|
||||
class AfterFirstRefreshTests(ImportTestCase):
|
||||
"""The way to move: import, let the new feeds be fetched, import again."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue