8 from django.core.management.base import BaseCommand, CommandError
9 from django.utils.dateparse import parse_datetime
11 from nonstop.models import Artist, SomaLogLine, Track
13 logger = logging.getLogger('airtime_tracker')
16 def parse_duration(duration):
17 # ex: "00:01:30.07025" -> 90.07025 (seconds)
18 parts = [float(x) for x in duration.split(':')]
19 return datetime.timedelta(seconds=parts[2] + (parts[1] * 60) + (parts[0] * 60 * 60))
22 class Command(BaseCommand):
23 def add_arguments(self, parser):
24 parser.add_argument('--live-info-url', help='https://foobar.airtime.pro/api/live-info-v2')
26 def handle(self, verbosity, live_info_url, **options):
28 raise CommandError('missing --live-info-url')
30 # run until killed, and starts back where it was killed
33 logger.debug('Checking API')
35 resp = requests.get(live_info_url, timeout=10)
36 except requests.RequestException as err:
37 logger.error('Got error requesting API (%s)', err)
41 logger.error('Got invalid API answer (%s)', resp.status_code)
45 current_show = (resp.json().get('shows') or {}).get('current')
46 except json.JSONDecodeError:
47 logger.error('Got invalid API answer (not JSON)')
50 if current_show and current_show.get('auto_dj'):
51 # API returns metadata for previous/next track and a useless
52 # "livestream" entry for the currently playing track; so we
54 logger.debug('(autodj mode)')
55 parts = ('previous', 'current', 'next')
60 for part_name in parts:
61 part = resp.json().get('tracks', {}).get(part_name)
64 starts = part.get('starts')
65 if part.get('type') == 'track':
66 metadata = part.get('metadata')
67 if not metadata.get('length'):
69 if not metadata.get('track_title'):
71 duration = parse_duration(metadata.get('length'))
72 if duration < datetime.timedelta(seconds=30):
74 if last_skipped != metadata.get('track_title'):
75 last_skipped = metadata.get('track_title')
78 'Got %s but skipped as too short (%s seconds)',
79 metadata.get('track_title'),
80 duration.total_seconds(),
82 continue # skip what's probably a jingle
83 if duration > datetime.timedelta(seconds=3000):
85 if last_skipped != metadata.get('track_title'):
86 last_skipped = metadata.get('track_title')
89 'Got %s but skipped as too long (%s seconds)',
90 metadata.get('track_title'),
91 duration.total_seconds(),
93 continue # skip what's probably a full show
94 if metadata.get('artist_name'):
95 artist, created = Artist.objects.get_or_create(
96 name=html.unescape(metadata.get('artist_name'))
99 continue # skip if there's no artist in metadata
101 track, created = Track.objects.get_or_create(
103 title=html.unescape(metadata.get('track_title')),
107 logger.info('New track: %s', track)
108 logline, created = SomaLogLine.objects.get_or_create(
109 track=track, play_timestamp=parse_datetime(starts)
112 logger.info('Playing at %s: %s', starts, track)
114 if not resp.json().get('tracks', {}).get('next'):
115 logger.warning('No known next track')
119 next_datetime = parse_datetime(resp.json().get('tracks', {}).get('next', {}).get('starts'))
120 wait_until = min(datetime.datetime.now() + datetime.timedelta(minutes=5), next_datetime)
121 waiting_time = (wait_until - datetime.datetime.now()).total_seconds()
122 # wait at least 1 second but maximum 5 minutes
123 waiting_time = max((min((waiting_time, 300)), 1))
124 logger.debug('Waiting for %d seconds (announced change is %s)', waiting_time, next_datetime)
125 time.sleep(waiting_time)