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.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 .models import GpodderDevice, GpodderSubscriptionChange, GpodderEpisodeAction
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ def episode_actions(request, username):
|
|||
if ep:
|
||||
pos = int(a['position'])
|
||||
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(
|
||||
user=request.user, episode=ep,
|
||||
defaults={'position_seconds': pos, 'played': played}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,16 @@ class PodcastEpisode(models.Model):
|
|||
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):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='episode_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):
|
||||
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.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)
|
||||
|
||||
|
||||
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):
|
||||
"""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 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
|
||||
|
||||
feedext.install()
|
||||
|
|
@ -101,9 +101,21 @@ def _enqueue(user, episode_ids, where, current_hint=None):
|
|||
ignore_conflicts=True,
|
||||
)
|
||||
_write_queue_order(user, queued[:at] + new + queued[at:])
|
||||
_leave_inbox(user, 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -1150,10 +1162,11 @@ def save_progress(request):
|
|||
episode=episode,
|
||||
)
|
||||
progress.position_seconds = position
|
||||
|
||||
# Auto-mark played at 90%
|
||||
if episode.duration_seconds and episode.duration_seconds > 0:
|
||||
if position >= episode.duration_seconds * 0.9:
|
||||
# Started: no longer new.
|
||||
if position > 0:
|
||||
progress.dismissed = True
|
||||
# Within the last seconds: played (same rule as the apps).
|
||||
if counts_as_played(position, episode.duration_seconds):
|
||||
progress.played = True
|
||||
|
||||
progress.save()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue