]> git.0d.be Git - django-panik-nonstop.git/commitdiff
stamina
authorFrédéric Péters <fpeters@0d.be>
Sun, 17 May 2020 18:25:44 +0000 (20:25 +0200)
committerFrédéric Péters <fpeters@0d.be>
Thu, 21 May 2020 07:11:54 +0000 (09:11 +0200)
nonstop/management/commands/stamina.py [new file with mode: 0644]

diff --git a/nonstop/management/commands/stamina.py b/nonstop/management/commands/stamina.py
new file mode 100644 (file)
index 0000000..6b9a69a
--- /dev/null
@@ -0,0 +1,234 @@
+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