"""The Podcast-Radio (docs/podcasts.md): the Strom played like a station, leaving no trace. Which episode comes next: the newest (publication date) Strom episode that is neither played, nor listened into, nor done for the radio (PodcastRadioHeard) — so one that comes out while another plays is next, and otherwise the radio goes back in time. Several of one podcast in a row are mixed with the others waiting: the next is the newest of another podcast than the one before, if there is one. GET /podcasts/radio/?after=&skip=&limit=5 → {episodes: [...]} (episode-list rows) skip: ones the device is done with whose /heard/ may not have arrived yet POST /podcasts/radio/heard/ {"episode_id", "after"?} → {ok, episodes} (the next ones) """ import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from .models import PodcastEpisode, PodcastRadioHeard from .views import _episode_list_data LIMIT = 5 MAX_LIMIT = 20 MAX_SKIP = 50 # How far down the newest the mixing looks for another podcast. WINDOW = 200 def next_episodes(user, after=None, limit=LIMIT, skip=()): """The radio's next episodes, in the order to play; `after`: the episode playing (or played last) — not itself again, and not its podcast next if another is waiting; `skip`: not these either.""" candidates, _ = _episode_list_data(user, ['stream', 'untouched'], sort='new', limit=WINDOW, radio=True) skip = set(skip) | ({after} if after else set()) candidates = [e for e in candidates if e['id'] not in skip] last_feed = None if after: last_feed = PodcastEpisode.objects.filter(pk=after, feed__user=user).values_list('feed_id', flat=True).first() out = [] while candidates and len(out) < limit: pick = next((e for e in candidates if e['feed__id'] != last_feed), candidates[0]) candidates.remove(pick) out.append(pick) last_feed = pick['feed__id'] return out def _after(value): if value in (None, ''): return None after = int(value) if after <= 0: raise ValueError return after @csrf_exempt @require_http_methods(['GET']) def radio(request): if not request.user.is_authenticated: return JsonResponse({'error': 'authentication required'}, status=401) try: after = _after(request.GET.get('after')) limit = min(max(int(request.GET.get('limit') or LIMIT), 1), MAX_LIMIT) skip = [int(i) for i in request.GET.get('skip', '').split(',') if i.strip()][:MAX_SKIP] except ValueError: return JsonResponse({'error': 'after, skip and limit must be numbers'}, status=400) return JsonResponse({'episodes': next_episodes(request.user, after, limit, skip)}) @csrf_exempt @require_http_methods(['POST']) def radio_heard(request): """The radio is done with an episode (ran to its end, or skipped): it never plays it again. Nothing else about the episode changes. Answers with the next ones (after it).""" if not request.user.is_authenticated: return JsonResponse({'error': 'authentication required'}, status=401) try: body = json.loads(request.body) episode_id = int(body['episode_id']) after = _after(body.get('after')) except (ValueError, KeyError, TypeError): return JsonResponse({'error': 'episode_id required'}, status=400) episode = PodcastEpisode.objects.filter(pk=episode_id, feed__user=request.user).first() if episode is None: return JsonResponse({'error': 'unknown episode'}, status=404) PodcastRadioHeard.objects.get_or_create(user=request.user, episode=episode) return JsonResponse({'ok': True, 'episodes': next_episodes(request.user, after or episode_id)})