]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/management/commands/airtime_tracker.py
2ac58002669c97bcb49ff8f5e1b5f646080212f2
[django-panik-nonstop.git] / nonstop / management / commands / airtime_tracker.py
1 import datetime
2 import html
3 import json
4 import logging
5 import time
6
7 import requests
8 from django.core.management.base import BaseCommand, CommandError
9 from django.utils.dateparse import parse_datetime
10
11 from nonstop.models import Artist, SomaLogLine, Track
12
13 logger = logging.getLogger('airtime_tracker')
14
15
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))
20
21
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')
25
26     def handle(self, verbosity, live_info_url, **options):
27         if not live_info_url:
28             raise CommandError('missing --live-info-url')
29
30         # run until killed, and starts back where it was killed
31         last_skipped = None
32         while True:
33             logger.debug('Checking API')
34             try:
35                 resp = requests.get(live_info_url, timeout=10)
36             except requests.RequestException as err:
37                 logger.error('Got error requesting API (%s)', err)
38                 time.sleep(60)
39                 continue
40             if not resp.ok:
41                 logger.error('Got invalid API answer (%s)', resp.status_code)
42                 time.sleep(60)
43                 continue
44             try:
45                 current_show = (resp.json().get('shows') or {}).get('current')
46             except json.JSONDecodeError:
47                 logger.error('Got invalid API answer (not JSON)')
48                 time.sleep(60)
49                 continue
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
53                 # look at them all.
54                 logger.debug('(autodj mode)')
55                 parts = ('previous', 'current', 'next')
56             elif current_show:
57                 parts = ('current',)
58             else:
59                 parts = ()
60             for part_name in parts:
61                 part = resp.json().get('tracks', {}).get(part_name)
62                 if part is None:
63                     continue
64                 starts = part.get('starts')
65                 if part.get('type') == 'track':
66                     metadata = part.get('metadata')
67                     if not metadata.get('length'):
68                         continue
69                     if not metadata.get('track_title'):
70                         continue
71                     duration = parse_duration(metadata.get('length'))
72                     if duration < datetime.timedelta(seconds=30):
73                         log = logger.debug
74                         if last_skipped != metadata.get('track_title'):
75                             last_skipped = metadata.get('track_title')
76                             log = logger.info
77                         log(
78                             'Got %s but skipped as too short (%s seconds)',
79                             metadata.get('track_title'),
80                             duration.total_seconds(),
81                         )
82                         continue  # skip what's probably a jingle
83                     if duration > datetime.timedelta(seconds=3000):
84                         log = logger.debug
85                         if last_skipped != metadata.get('track_title'):
86                             last_skipped = metadata.get('track_title')
87                             log = logger.info
88                         log(
89                             'Got %s but skipped as too long (%s seconds)',
90                             metadata.get('track_title'),
91                             duration.total_seconds(),
92                         )
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'))
97                         )
98                     else:
99                         continue  # skip if there's no artist in metadata
100                     last_skipped = None
101                     track, created = Track.objects.get_or_create(
102                         artist=artist,
103                         title=html.unescape(metadata.get('track_title')),
104                         duration=duration,
105                     )
106                     if created:
107                         logger.info('New track: %s', track)
108                     logline, created = SomaLogLine.objects.get_or_create(
109                         track=track, play_timestamp=parse_datetime(starts)
110                     )
111                     if created:
112                         logger.info('Playing at %s: %s', starts, track)
113
114             if not resp.json().get('tracks', {}).get('next'):
115                 logger.warning('No known next track')
116                 time.sleep(30)
117                 continue
118
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)