88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""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=<episode id>&skip=<ids>&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)})
|