Podcasts: Inbox und Gehört wie in AntennaPod
- Eingereihte und angefangene Folgen verlassen die Inbox (ausgeblendet, nicht gehört), statt dort neben der Warteschlange zu stehen. - Gehört heißt: höchstens 30 s vor dem Ende, wie in den Apps, statt bei 90 % (bei 2 h waren das 12 Minuten vor Schluss). Gilt auch für den gPodder-Sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c335ab4094
commit
f9b3a0ce04
4 changed files with 63 additions and 9 deletions
|
|
@ -8,7 +8,7 @@ from django.http import JsonResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from podcasts.models import PodcastFeed, PodcastEpisode, EpisodeProgress
|
from podcasts.models import PodcastFeed, PodcastEpisode, EpisodeProgress, counts_as_played
|
||||||
from podcasts.views import _refresh_feed
|
from podcasts.views import _refresh_feed
|
||||||
from .models import GpodderDevice, GpodderSubscriptionChange, GpodderEpisodeAction
|
from .models import GpodderDevice, GpodderSubscriptionChange, GpodderEpisodeAction
|
||||||
|
|
||||||
|
|
@ -235,7 +235,7 @@ def episode_actions(request, username):
|
||||||
if ep:
|
if ep:
|
||||||
pos = int(a['position'])
|
pos = int(a['position'])
|
||||||
dur = ep.duration_seconds or 0
|
dur = ep.duration_seconds or 0
|
||||||
played = dur > 0 and pos >= dur * 0.9
|
played = counts_as_played(pos, dur)
|
||||||
EpisodeProgress.objects.update_or_create(
|
EpisodeProgress.objects.update_or_create(
|
||||||
user=request.user, episode=ep,
|
user=request.user, episode=ep,
|
||||||
defaults={'position_seconds': pos, 'played': played}
|
defaults={'position_seconds': pos, 'played': played}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,16 @@ class PodcastEpisode(models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
# An episode counts as played this close to its end (AntennaPod's default),
|
||||||
|
# the same rule as the apps' "als gehört zählen".
|
||||||
|
PLAYED_BEFORE_END_SECONDS = 30
|
||||||
|
|
||||||
|
|
||||||
|
def counts_as_played(position_seconds, duration_seconds):
|
||||||
|
return bool(duration_seconds) and duration_seconds > 0 and \
|
||||||
|
position_seconds >= max(duration_seconds - PLAYED_BEFORE_END_SECONDS, 1)
|
||||||
|
|
||||||
|
|
||||||
class EpisodeProgress(models.Model):
|
class EpisodeProgress(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='episode_progress')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='episode_progress')
|
||||||
episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='progress')
|
episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='progress')
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class QueuePositionTests(TestCase):
|
||||||
|
|
||||||
def test_after_current_falls_back_to_last_listened(self):
|
def test_after_current_falls_back_to_last_listened(self):
|
||||||
self.add(self.eps[0], self.eps[1], self.eps[2])
|
self.add(self.eps[0], self.eps[1], self.eps[2])
|
||||||
EpisodeProgress.objects.create(user=self.user, episode=self.eps[2], position_seconds=30)
|
EpisodeProgress.objects.update_or_create(user=self.user, episode=self.eps[2], defaults={'position_seconds': 30})
|
||||||
self.add(self.eps[3], where='after_current')
|
self.add(self.eps[3], where='after_current')
|
||||||
self.assertEqual(self.queue(), [e.id for e in (self.eps[0], self.eps[1], self.eps[2], self.eps[3])])
|
self.assertEqual(self.queue(), [e.id for e in (self.eps[0], self.eps[1], self.eps[2], self.eps[3])])
|
||||||
|
|
||||||
|
|
@ -104,6 +104,37 @@ class QueuePositionTests(TestCase):
|
||||||
self.assertEqual(bad.status_code, 400)
|
self.assertEqual(bad.status_code, 400)
|
||||||
|
|
||||||
|
|
||||||
|
class InboxRuleTests(TestCase):
|
||||||
|
"""Queued or started episodes leave the inbox; played means the last 30 s."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(username='alice', password='pw12345678')
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
feed = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/f', title='F')
|
||||||
|
self.eps = [PodcastEpisode.objects.create(feed=feed, guid=f'g{i}', title=f'E{i}', duration_seconds=3600,
|
||||||
|
audio_url=f'https://example.org/{i}.mp3') for i in range(3)]
|
||||||
|
|
||||||
|
def inbox(self):
|
||||||
|
return {e['id'] for e in self.client.get('/podcasts/inbox/').json()['episodes']}
|
||||||
|
|
||||||
|
def post(self, url, body):
|
||||||
|
return self.client.post(url, json.dumps(body), content_type='application/json')
|
||||||
|
|
||||||
|
def test_queued_and_started_leave_the_inbox(self):
|
||||||
|
self.assertEqual(self.inbox(), {e.id for e in self.eps})
|
||||||
|
self.post('/podcasts/queue/add/', {'episode_ids': [self.eps[0].id]})
|
||||||
|
self.post('/podcasts/progress/save/', {'episode_id': self.eps[1].id, 'position_seconds': 12})
|
||||||
|
self.assertEqual(self.inbox(), {self.eps[2].id})
|
||||||
|
p = EpisodeProgress.objects.get(user=self.user, episode=self.eps[0])
|
||||||
|
self.assertFalse(p.played)
|
||||||
|
|
||||||
|
def test_played_within_the_last_30_seconds(self):
|
||||||
|
r = self.post('/podcasts/progress/save/', {'episode_id': self.eps[0].id, 'position_seconds': 3500})
|
||||||
|
self.assertFalse(r.json()['played']) # 90 % but 100 s left: not yet
|
||||||
|
r = self.post('/podcasts/progress/save/', {'episode_id': self.eps[0].id, 'position_seconds': 3575})
|
||||||
|
self.assertTrue(r.json()['played'])
|
||||||
|
|
||||||
|
|
||||||
class AutoQueueTests(TestCase):
|
class AutoQueueTests(TestCase):
|
||||||
"""New episodes of an auto_queue feed land where the user's setting says."""
|
"""New episodes of an auto_queue feed land where the user's setting says."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from . import chapters as chapters_mod
|
from . import chapters as chapters_mod
|
||||||
from . import feedext
|
from . import feedext
|
||||||
from .models import EpisodeProgress, PodcastEpisode, PodcastFeed, PodcastQueue, PodcastSettings
|
from .models import EpisodeProgress, PodcastEpisode, PodcastFeed, PodcastQueue, PodcastSettings, counts_as_played
|
||||||
from .shownotes import extract_timestamps, sanitize_html
|
from .shownotes import extract_timestamps, sanitize_html
|
||||||
|
|
||||||
feedext.install()
|
feedext.install()
|
||||||
|
|
@ -101,9 +101,21 @@ def _enqueue(user, episode_ids, where, current_hint=None):
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
_write_queue_order(user, queued[:at] + new + queued[at:])
|
_write_queue_order(user, queued[:at] + new + queued[at:])
|
||||||
|
_leave_inbox(user, new)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
def _leave_inbox(user, episode_ids):
|
||||||
|
"""Queued or started episodes are no longer new (AntennaPod's rule): out
|
||||||
|
of the inbox, without counting as played."""
|
||||||
|
EpisodeProgress.objects.filter(user=user, episode_id__in=episode_ids, dismissed=False).update(dismissed=True)
|
||||||
|
have = set(EpisodeProgress.objects.filter(user=user, episode_id__in=episode_ids).values_list('episode_id', flat=True))
|
||||||
|
EpisodeProgress.objects.bulk_create(
|
||||||
|
[EpisodeProgress(user=user, episode_id=ep_id, dismissed=True) for ep_id in episode_ids if ep_id not in have],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Duration helper
|
# Duration helper
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -1150,11 +1162,12 @@ def save_progress(request):
|
||||||
episode=episode,
|
episode=episode,
|
||||||
)
|
)
|
||||||
progress.position_seconds = position
|
progress.position_seconds = position
|
||||||
|
# Started: no longer new.
|
||||||
# Auto-mark played at 90%
|
if position > 0:
|
||||||
if episode.duration_seconds and episode.duration_seconds > 0:
|
progress.dismissed = True
|
||||||
if position >= episode.duration_seconds * 0.9:
|
# Within the last seconds: played (same rule as the apps).
|
||||||
progress.played = True
|
if counts_as_played(position, episode.duration_seconds):
|
||||||
|
progress.played = True
|
||||||
|
|
||||||
progress.save()
|
progress.save()
|
||||||
return JsonResponse({'ok': True, 'played': progress.played})
|
return JsonResponse({'ok': True, 'played': progress.played})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue