Commit 2cd269c5 authored by fred's avatar fred

stamina

parent 113cdeaf
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment