]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/utils.py
add weight settings for track parameters
[django-panik-nonstop.git] / nonstop / utils.py
1 import datetime
2 import os
3 import random
4 import shutil
5 import socket
6 import time
7
8 from django.template import loader
9 from django.utils.timezone import now
10 import xml.etree.ElementTree as ET
11
12 from emissions.models import Diffusion, Schedule
13 from .models import SomaLogLine, ScheduledDiffusion, Jingle, RecurringStreamOccurence, Track
14 from .app_settings import app_settings
15
16
17 class SomaException(Exception):
18     pass
19
20
21 def get_current_nonstop_track():
22     try:
23         soma_log_line = SomaLogLine.objects.select_related().order_by('-play_timestamp')[0]
24     except IndexError:
25         # nothing yet
26         return {}
27     if soma_log_line.play_timestamp < (datetime.datetime.now() - datetime.timedelta(hours=1)):
28         # last known line is way too old
29         return {}
30     if not soma_log_line.on_air:
31         # nonstop should be on air but it's not :/
32         return {}
33     d = {}
34     current_nonstop_file = soma_log_line.filepath
35     if current_nonstop_file:
36         if 'Tranches/' not in current_nonstop_file.filepath and (
37                 'tracks/' not in current_nonstop_file.filepath):
38             # nonstop is playing but it's not a nonstop track :/
39             return {}
40     current_track = soma_log_line.get_track()
41     if current_track is None:
42         # nonstop is playing a nonstop track, but it's unknown :/
43         return {}
44     d = {'track_title': current_track.title}
45     if current_track.artist:
46         d['track_artist'] = current_track.artist.name
47     return d
48
49
50 def get_diffusion_file_path(diffusion):
51     return u'diffusions-auto/%s--%s' % (
52             diffusion.datetime.strftime('%Y%m%d-%H%M'),
53             diffusion.episode.emission.slug)
54
55
56 def is_already_in_soma(diffusion):
57     if isinstance(diffusion, Diffusion):
58         if ScheduledDiffusion.objects.filter(diffusion=diffusion).exists():
59             return True
60     elif isinstance(diffusion, Schedule):
61         if RecurringStreamOccurence.objects.filter(diffusion__schedule=diffusion).exists():
62             return True
63     return False
64
65
66 def soma_connection():
67     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68     s.connect(('soma', 12521))
69     s.recv(1024)  # -> b'soma 2.5 NO_SSL\n'
70     s.sendall(b'100 - Ok\n')
71     s.recv(1024)  # -> b'100 - Welcome to soma daemon\n'
72     s.sendall(b'\n')  # (empty password)
73     if s.recv(1024) != b'100 - Ok\n':
74         raise SomaException('failed to initialize soma connection')
75     return s
76
77
78 def add_soma_diffusion(diffusion):
79     context = {}
80     context['diffusion'] = diffusion
81     context['jingle'] = diffusion.jingle
82     context['episode'] = diffusion.episode
83     context['start'] = diffusion.datetime
84     context['end'] = diffusion.end_datetime
85
86     if not diffusion.is_stream():
87         # program a soundfile
88         soundfile = diffusion.episode.soundfile_set.filter(fragment=False)[0]
89         diffusion_path = get_diffusion_file_path(diffusion)
90
91         # copy file
92         if not os.path.exists(app_settings.LOCAL_BASE_PATH):
93             raise SomaException('soma directory is not available')
94         local_diffusion_path = os.path.join(app_settings.LOCAL_BASE_PATH, diffusion_path)
95         if os.path.exists(local_diffusion_path):
96             for filename in os.listdir(local_diffusion_path):
97                 os.unlink(os.path.join(local_diffusion_path, filename))
98         else:
99             os.mkdir(os.path.join(app_settings.LOCAL_BASE_PATH, diffusion_path))
100         try:
101             shutil.copyfile(soundfile.file.path,
102                 os.path.join(app_settings.LOCAL_BASE_PATH, diffusion_path, os.path.basename(soundfile.file.path)))
103         except IOError:
104             try:
105                 os.rmdir(os.path.join(app_settings.LOCAL_BASE_PATH, diffusion_path))
106             except IOError:
107                 pass
108             raise SomaException('error copying file to soma')
109
110         context['diffusion_path'] = diffusion_path
111         # end should be a bit before the real end of file so the same file doesn't
112         # get repeated but shouldn't be less or equal than start date or soma would
113         # loop the file
114         context['end'] = diffusion.datetime + datetime.timedelta(seconds=
115                 max(((soundfile.duration or 480) - 180), 60))
116
117     # create palinsesti
118     palinsesti_template = loader.get_template('nonstop/soma_palinsesti.xml')
119
120     palinsesti = palinsesti_template.render(context)
121     palinsesti_xml = ET.fromstring(palinsesti.encode('utf-8'))
122
123     palinsesto_xml = get_palinsesto_xml()
124     palinsesto_xml.append(palinsesti_xml)
125     send_palinsesto_xml(palinsesto_xml)
126     diffusion.added_to_nonstop_timestamp = now()
127     diffusion.save()
128
129
130 def remove_soma_diffusion(diffusion):
131     palinsesto_xml = get_palinsesto_xml()
132     for palinsesto in palinsesto_xml.findall('Palinsesto'):
133         if palinsesto.findall('Description')[0].text.startswith(diffusion.soma_id):
134             palinsesto_xml.remove(palinsesto)
135     send_palinsesto_xml(palinsesto_xml)
136
137
138 def send_palinsesto_xml(palinsesto_xml):
139     with soma_connection() as s:
140         s.sendall(b'106 - Switch to a New Palinsesto Request\n')
141         if s.recv(1024) != b'100 - Ok\n':
142             raise SomaException('failed to switch palinsesto')
143         s.sendall(ET.tostring(palinsesto_xml))
144
145     # give it some time (...)
146     time.sleep(3)
147     with soma_connection() as s:
148         s.sendall(b'122 - Set the current Palinsesto as Default\n')
149         if s.recv(1024) != b'100 - Ok\n':
150             raise SomaException('failed to set current palinsesto as default')
151
152
153 def get_palinsesto_xml():
154     with soma_connection() as s:
155         s.sendall(b'109 - Get the current palinsesto\n')
156         palinsesto_bytes = b''
157         while True:
158             new_bytes = s.recv(200000)
159             if not new_bytes:
160                 break
161             palinsesto_bytes += new_bytes
162         if not palinsesto_bytes.startswith(b'100 - Ok\n'):
163             raise SomaException('failed to get palinsesto')
164         palinsesto_bytes = palinsesto_bytes[9:]
165     palinsesto_xml = ET.fromstring(palinsesto_bytes)
166     return palinsesto_xml
167
168
169 class Tracklist:
170     def __init__(self, zone_settings, zone_ids, recent_tracks_id=None, filter_kwargs={}, k=30):
171         self.zone_settings = zone_settings
172         self.zone_ids = zone_ids
173         self.playlist = []
174         self.recent_tracks_id = recent_tracks_id or []
175         self.filter_kwargs = filter_kwargs
176         self.k = k
177
178     def append(self, track):
179         # track or jingle
180         self.playlist.append(track)
181
182     def pop(self):
183         return self.playlist.pop() if self.playlist else None
184
185     def get_recent_track_ids(self):
186         return self.recent_tracks_id + [x.id for x in self.playlist if isinstance(x, Track)]
187
188     def get_duration(self):
189         return sum([x.duration for x in self.playlist], datetime.timedelta(seconds=0))
190
191     def get_random_tracks(self, k=30):
192         weights = self.zone_settings.weights
193
194         while True:
195             # pick tracks from db
196             tracks = Track.objects.filter(
197                     nonstop_zones__in=self.zone_ids,
198                     duration__isnull=False,
199                     **self.filter_kwargs).exclude(
200                             id__in=self.get_recent_track_ids()
201                             ).order_by('?')[:k*10]
202             if len(tracks) == 0:
203                 self.recent_tracks_id = self.recent_tracks_id[:len(self.recent_tracks_id) // 2]
204                 continue
205
206             def compute_weight(track):
207                 weight = 0
208                 for weight_key, weight_value in weights.items():
209                     if track.match_criteria(weight_key):
210                         weight += weight_value
211                 if weight < 0:
212                     weight = 1 + (weight / 20)
213                 else:
214                     weight = 1 + (weight / 2)
215                 return weight
216
217             track_weights = [compute_weight(x) for x in tracks]
218             tracks = random.choices(tracks, weights=track_weights, k=k)
219
220             seen = set()
221             for track in tracks:
222                 if track in seen:
223                     continue
224                 yield track
225                 seen.add(track)