--- /dev/null
+import asyncio
+import datetime
+import random
+
+from django.core.management.base import BaseCommand
+
+from emissions.models import Nonstop
+from nonstop.models import Track, ScheduledDiffusion
+
+
+class Command(BaseCommand):
+ last_jingle_datetime = None
+ quit = False
+
+ def handle(self, verbosity, **kwargs):
+ try:
+ asyncio.run(self.main(), debug=True)
+ except KeyboardInterrupt:
+ if not self.play_task.done():
+ self.play_task.cancel()
+ if not self.recompute_slots_task.done():
+ self.recompute_slots_task.done()
+
+ def get_playlist(self, zone, start_datetime, end_datetime):
+ current_datetime = start_datetime
+ if self.last_jingle_datetime is None:
+ self.last_jingle_datetime = current_datetime
+ playlist = []
+ adjustment_counter = 0
+ try:
+ jingles = list(zone.nonstopzonesettings_set.first().jingles.all())
+ except AttributeError:
+ jingles = []
+
+ while current_datetime < end_datetime and adjustment_counter < 5:
+
+ if jingles and current_datetime - self.last_jingle_datetime > datetime.timedelta(minutes=20):
+ # jingle time, every ~20 minutes
+ playlist.append(random.choice(jingles))
+ self.last_jingle_datetime = current_datetime
+
+ remaining_time = (end_datetime - current_datetime)
+ track = Track.objects.filter(
+ nonstop_zones=zone,
+ duration__isnull=False).exclude(
+ id__in=[x.id for x in playlist if isinstance(x, Track)]
+ ).order_by('?').first()
+ playlist.append(track)
+ current_datetime = start_datetime + sum(
+ [x.duration for x in playlist], datetime.timedelta(seconds=0))
+ if current_datetime > end_datetime:
+ # last track overshot
+ # 1st strategy: remove last track and try to get a track with
+ # exact remaining time
+ playlist = playlist[:-1]
+ current_datetime = start_datetime + sum(
+ [x.duration for x in playlist], datetime.timedelta(seconds=0))
+ track = Track.objects.filter(
+ nonstop_zones=zone,
+ duration__gte=remaining_time,
+ duration__lt=remaining_time + datetime.timedelta(seconds=1)
+ ).exclude(
+ id__in=[x.id for x in playlist if isinstance(x, Track)]
+ ).order_by('?').first()
+ if track:
+ # found a track
+ playlist.append(track)
+ current_datetime = start_datetime + sum(
+ [x.duration for x in playlist], datetime.timedelta(seconds=0))
+ else:
+ # fallback strategy: didn't find track of expected duration,
+ # reduce playlist further
+ adjustment_counter += 1
+ playlist = playlist[:-1]
+ current_datetime = start_datetime + sum(
+ [x.duration for x in playlist], datetime.timedelta(seconds=0))
+
+ print('computed playlist:')
+ current_datetime = start_datetime
+ for track in playlist:
+ print(' ', current_datetime, track.duration, track.title)
+ current_datetime += track.duration
+ print(' ', current_datetime, '---')
+ print(' adjustment_counter:', adjustment_counter)
+
+ return playlist
+
+ async def player_process(self, cmd, timeout=None):
+ self.player = await asyncio.create_subprocess_shell(
+ cmd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE)
+ if timeout is None:
+ await self.player.communicate()
+ else:
+ try:
+ await asyncio.wait_for(self.player.communicate(), timeout=timeout)
+ except asyncio.TimeoutError:
+ pass
+ self.player = None
+
+ async def play(self, slot):
+ now = datetime.datetime.now()
+ if isinstance(slot, Nonstop):
+ self.playlist = self.get_playlist(slot, now, slot.end_datetime)
+ self.playhead = 0
+ while True:
+ now = datetime.datetime.now()
+ try:
+ track = self.playlist[self.playhead]
+ except IndexError:
+ break
+ self.current_track_start_datetime = now
+ print(now, track.title, track.duration,
+ '- future tracks:', [x.title for x in self.playlist[self.playhead + 1:self.playhead + 3]])
+ cmd = 'sleep %s # %s' % (track.duration.seconds, track.title)
+ await self.player_process(cmd)
+ self.playhead += 1
+ elif slot.is_stream():
+ print(now, 'playing stream', slot.stream)
+ # TODO: jingle
+ cmd = 'sleep 86400 # stream' # will never stop by itself
+ await self.player_process(cmd, timeout=slot.duration)
+ else:
+ print(now, 'playing sound', slot.episode)
+ # TODO: jingle
+ cmd = 'sleep %s # %s' % (slot.soundfile.duration, slot.episode)
+ await self.player_process(cmd)
+
+ def recompute_playlist(self):
+ current_track = self.playlist[self.playhead]
+ print('recompute_playlist, from', current_track.title, self.current_track_start_datetime + current_track.duration, 'to', self.slot.end_datetime)
+ playlist = self.get_playlist(self.slot,
+ self.current_track_start_datetime + current_track.duration, self.slot.end_datetime)
+ if playlist:
+ self.playlist[self.playhead + 1:] = playlist
+
+ def recompute_slots(self):
+ now = datetime.datetime.now()
+ # print(now, 'recompute_slots')
+ diffusion = ScheduledDiffusion.objects.filter(
+ diffusion__datetime__gt=now - datetime.timedelta(days=1),
+ diffusion__datetime__lt=now).last()
+ if diffusion and diffusion.end_datetime > now:
+ self.slot = diffusion
+ else:
+ nonstops = list(Nonstop.objects.all().order_by('start'))
+ nonstops = [x for x in nonstops if x.start != x.end] # disabled zones
+ try:
+ self.slot = [x for x in nonstops if x.start < now.time()][-1]
+ except IndexError:
+ self.slot = nonstops[0]
+ try:
+ next_slot = nonstops[nonstops.index(self.slot) + 1]
+ except IndexError:
+ next_slot = nonstops[0]
+ self.slot.datetime = now.replace(
+ hour=self.slot.start.hour,
+ minute=self.slot.start.minute)
+ self.slot.end_datetime = now.replace(
+ hour=next_slot.start.hour,
+ minute=next_slot.start.minute,
+ second=0,
+ microsecond=0)
+ if self.slot.end_datetime < self.slot.datetime:
+ self.slot.end_datetime += datetime.timedelta(days=1)
+
+ diffusion = ScheduledDiffusion.objects.filter(
+ diffusion__datetime__gt=now,
+ diffusion__datetime__lt=self.slot.end_datetime).first()
+ if diffusion:
+ self.slot.end_datetime = diffusion.datetime
+
+ async def recompute_slots_loop(self):
+ print('recompute_slots_loop')
+ now = datetime.datetime.now()
+ sleep = (60 - now.second) % 10 # adjust to awake at :00
+ while not self.quit:
+ await asyncio.sleep(sleep)
+ sleep = 10 # next cycles every 10 seconds
+ current_slot = self.slot
+ self.recompute_slots()
+ expected_slot = self.slot
+ if current_slot != expected_slot:
+ print('unexpected change', current_slot, 'vs', expected_slot)
+ if isinstance(current_slot, Nonstop) and not isinstance(expected_slot, Nonstop):
+ # interrupt nonstop
+ print('interrupting nonstop')
+ self.play_task.cancel()
+ elif current_slot.end_datetime > expected_slot.end_datetime:
+ print('change in end time, from %s to %s' %
+ (current_slot.end_datetime, expected_slot.end_datetime))
+ if expected_slot.end_datetime - datetime.datetime.now() > datetime.timedelta(minutes=5):
+ # more than 5 minutes left, recompute playlist
+ self.recompute_playlist()
+
+ async def handle_connection(self, reader, writer):
+ data = await reader.read(100)
+ message = data.decode().strip()
+ response = 'err'
+ if message == 'playing?':
+ response = '%s' % self.slot
+ writer.write(response.encode('utf-8'))
+ await writer.drain()
+ writer.close()
+
+ async def main(self):
+ now = datetime.datetime.now()
+ self.recompute_slots()
+ server = await asyncio.start_server(self.handle_connection, '127.0.0.1', 8888)
+ async with server:
+ asyncio.create_task(server.serve_forever())
+
+ self.recompute_slots_task = asyncio.create_task(self.recompute_slots_loop())
+ while True:
+ duration = (self.slot.end_datetime - now).seconds
+ print('next sure slot', duration, self.slot.end_datetime)
+ if duration < 2:
+ # next slot is very close, wait for it
+ await asyncio.sleep(duration)
+ self.recompute_slots()
+ self.play_task = asyncio.create_task(self.play(self.slot))
+ try:
+ await self.play_task
+ self.recompute_slots()
+ except asyncio.CancelledError as exc:
+ print('exc:', exc)
+ if self.player and self.player.returncode is None: # not finished
+ self.player.kill()
+ except KeyboardInterrupt:
+ self.quit = True
+ break
+ self.quit = True
+ await self.recompute_slots_task