]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/models.py
add weight settings for track parameters
[django-panik-nonstop.git] / nonstop / models.py
1 import collections
2 import datetime
3 import json
4 import os
5 import random
6
7 from django.conf import settings
8 from django.core.urlresolvers import reverse
9 from django.db import models
10 from django.db.models.signals import post_delete, post_save
11 from django.dispatch import receiver
12 from django.utils.timezone import now
13 from django.utils.translation import ugettext_lazy as _
14
15 from .app_settings import app_settings
16
17
18 TRANCHE_SLUG_DIR_MAPPING = {
19     'acouphene': 'Acouphene',
20     'biodiversite': 'Biodiversite',
21     'l-heure-de-pointe': 'Heure_de_pointe',
22     'hop-bop-co': 'Hop_Bop_and_co',
23     'la-panique': 'la_panique',
24     'le-mange-disque': 'Mange_Disque',
25     'matin-tranquille': 'Matins_tranquilles',
26     'reveries': 'Reveries',
27     'up-beat-tempo': 'Up_Beat_Tempo',
28 }
29
30
31
32 class Artist(models.Model):
33     name = models.CharField(_('Name'), max_length=255)
34
35     class Meta:
36         ordering = ['name']
37
38     def __str__(self):
39         return self.name
40
41     def get_absolute_url(self):
42         return reverse('artist-view', kwargs={'pk': self.id})
43
44     def recent_diffusions(self):
45         return SomaLogLine.objects.filter(filepath__track__artist=self
46                 ).exclude(on_air=False).order_by('-play_timestamp')
47
48     def active_tracks(self):
49         return self.track_set.filter(nonstop_zones__isnull=False).distinct().order_by('title')
50
51     def available_tracks(self):
52         return self.track_set.filter(nonstop_zones__isnull=True).order_by('title')
53
54
55 class Album(models.Model):
56     name = models.CharField(_('Name'), max_length=255)
57
58
59 LANGUAGES = [
60     ('en', _('English')),
61     ('fr', _('French')),
62     ('nl', _('Dutch')),
63     ('other', _('Other')),
64     ('na', _('Not applicable')),
65 ]
66
67 class Track(models.Model):
68     title = models.CharField(_('Title'), max_length=255)
69     artist = models.ForeignKey(Artist, null=True)
70     album = models.ForeignKey(Album, null=True)
71     instru = models.BooleanField(_('Instru'), default=False)
72     language = models.CharField(_('Language'), max_length=10,
73             choices=LANGUAGES, blank=True)
74     sabam = models.BooleanField('SABAM', default=True)
75     cfwb = models.BooleanField('CFWB', default=False)
76     nonstop_zones = models.ManyToManyField('emissions.Nonstop', blank=True)
77
78     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
79     added_to_nonstop_timestamp = models.DateTimeField(null=True)
80     uploader = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
81     duration = models.DurationField(_('Duration'), null=True)
82
83     class Meta:
84         ordering = ['creation_timestamp']
85
86     def __str__(self):
87         return 'Track %s (%s)' % (self.title, self.artist or 'unknown')
88
89     def get_absolute_url(self):
90         return reverse('track-view', kwargs={'pk': self.id})
91
92     def recent_diffusions(self):
93         return SomaLogLine.objects.filter(filepath__track=self
94                 ).exclude(on_air=False).order_by('-play_timestamp')
95
96     def file_path(self):
97         nfile = None
98         for nfile in self.nonstopfile_set.all().order_by('creation_timestamp'):
99             if os.path.exists(nfile.get_local_filepath()):
100                 return nfile.get_local_filepath()
101         if nfile:
102             return nfile.get_local_filepath()
103         return None
104
105     def file_exists(self):
106         file_path = self.file_path()
107         if not file_path:
108             return False
109         try:
110             return os.path.exists(file_path)
111         except AttributeError:
112             return False
113
114     def sync_nonstop_zones(self):
115         current_zones = self.nonstop_zones.all()
116         if current_zones.count():
117             if not self.added_to_nonstop_timestamp:
118                 self.added_to_nonstop_timestamp = now()
119                 self.save()
120         else:
121             self.added_to_nonstop_timestamp = None
122             self.save()
123
124         if not self.file_exists():
125             return
126         nonstop_file = self.nonstopfile_set.order_by('creation_timestamp').last()
127         filename = nonstop_file.filename
128         from emissions.models import Nonstop
129
130         for zone in Nonstop.objects.all():
131             if not zone.slug in TRANCHE_SLUG_DIR_MAPPING:
132                 continue
133             zone_dir = TRANCHE_SLUG_DIR_MAPPING[zone.slug]
134             zone_path = os.path.join(app_settings.LOCAL_BASE_PATH, 'Tranches', zone_dir, filename)
135             if zone in current_zones:
136                 if not os.path.exists(zone_path):
137                     os.symlink(os.path.join('..', '..', nonstop_file.short), zone_path)
138             else:
139                 if os.path.exists(zone_path):
140                     os.unlink(zone_path)
141
142     def match_criteria(self, criteria):
143         return getattr(self, 'match_criteria_' + criteria)()
144
145     def match_criteria_lang_en(self):
146         return self.language == 'en'
147
148     def match_criteria_lang_fr(self):
149         return self.language == 'fr'
150
151     def match_criteria_lang_nl(self):
152         return self.language == 'nl'
153
154     def match_criteria_lang_other(self):
155         return self.language == 'other'
156
157     def match_criteria_instru(self):
158         return self.instru
159
160     def match_criteria_cfwb(self):
161         return self.cfwb
162
163
164 class NonstopFile(models.Model):
165     filepath = models.CharField(_('Filepath'), max_length=255)
166     filename = models.CharField(_('Filename'), max_length=255, null=True)
167     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
168     track = models.ForeignKey(Track, null=True)
169
170     @property
171     def short(self):
172         return self.filepath[len(app_settings.REMOTE_BASE_PATH):]
173
174     def set_track_filepath(self, filepath):
175         self.filepath = os.path.join(app_settings.REMOTE_BASE_PATH, 'tracks', filepath)
176         self.filename = os.path.basename(filepath)
177
178     def get_local_filepath(self):
179         if not self.short:
180             return None
181         return os.path.join(app_settings.LOCAL_BASE_PATH, self.short)
182
183
184 class SomaLogLine(models.Model):
185     class Meta:
186         verbose_name = _('Soma log line')
187         verbose_name_plural = _('Soma log lines')
188         ordering = ['play_timestamp']
189
190     filepath = models.ForeignKey(NonstopFile, null=True)
191     track = models.ForeignKey(Track, null=True)
192     play_timestamp = models.DateTimeField()
193     on_air = models.NullBooleanField('On Air')
194
195     def get_track(self):
196         if self.track_id:
197             return self.track
198         if self.filepath_id:
199             return self.filepath.track
200         return None
201
202
203 class Jingle(models.Model):
204     class Meta:
205         verbose_name = _('Jingle')
206         verbose_name_plural = _('Jingles')
207         ordering = ['label']
208
209     label = models.CharField(_('Label'), max_length=100)
210     filepath = models.CharField(_('File Path'), max_length=255)
211     duration = models.DurationField(_('Duration'), null=True, blank=True)
212     default_for_initial_diffusions = models.BooleanField(_('Default for initial diffusions'), default=False)
213     default_for_reruns = models.BooleanField(_('Default for reruns'), default=False)
214     default_for_streams = models.BooleanField(_('Default for streams'), default=False)
215
216     def __str__(self):
217         return self.label
218
219     @property
220     def short(self):
221         # property for compatibility with Track model
222         # for jingles self.filepath is actually only the last part of the path,
223         # ex: jingles panik/H_marimba_RP_chucho_zoe.wav
224         return self.filepath
225
226     def file_path(self):
227         # for compatibility with Track model method
228         return self.get_local_filepath()
229
230     def get_local_filepath(self):
231         if not self.short:
232             return None
233         return os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, self.short)
234
235     @property
236     def title(self):
237         return self.label
238
239
240 class Stream(models.Model):
241     class Meta:
242         verbose_name = _('Stream')
243         verbose_name_plural = _('Streams')
244         ordering = ['label']
245
246     label = models.CharField(_('Label'), max_length=100)
247     url = models.URLField(_('URL'), max_length=255)
248
249     def __str__(self):
250         return self.label
251
252
253 class ScheduledDiffusion(models.Model):
254     class Meta:
255         verbose_name = _('Scheduled diffusion')
256         verbose_name_plural = _('Scheduled diffusions')
257
258     diffusion = models.ForeignKey('emissions.Diffusion', null=True, blank=True, on_delete=models.SET_NULL)
259     jingle = models.ForeignKey(Jingle, null=True, blank=True)
260     stream = models.ForeignKey(Stream, null=True, blank=True)
261     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
262     added_to_nonstop_timestamp = models.DateTimeField(null=True)
263     auto_delayed = models.BooleanField(default=False)
264
265     def __str__(self):
266         return 'Diffusion of %s' % self.diffusion
267
268     @property
269     def datetime(self):
270         return self.diffusion.datetime
271
272     @property
273     def end_datetime(self):
274         if self.is_stream():
275             return self.diffusion.end_datetime
276         dt = self.diffusion.datetime
277         if self.jingle and self.jingle.duration:
278             dt += self.jingle.duration
279         dt += datetime.timedelta(seconds=self.soundfile.duration)
280         return dt
281
282     @property
283     def soundfile(self):
284         return self.diffusion.episode.soundfile_set.filter(fragment=False).first()
285
286     @property
287     def duration(self):
288         return (self.end_datetime - self.datetime).seconds
289
290     @property
291     def episode(self):
292         return self.diffusion.episode
293
294     @property
295     def soma_id(self):
296         if self.is_stream():
297             return '[stream:%s]' % self.id
298         else:
299             return '[sound:%s]' % self.id
300
301     def is_stream(self):
302         return bool(self.stream_id)
303
304     def get_jingle_filepath(self):
305         return self.jingle.get_local_filepath() if self.jingle_id else None
306
307     def file_path(self):
308         # TODO, copy and stuff
309         return self.soundfile.file.path
310
311
312 class NonstopZoneSettings(models.Model):
313     nonstop = models.ForeignKey('emissions.Nonstop', on_delete=models.CASCADE)
314     intro_jingle = models.ForeignKey(Jingle, blank=True, null=True, related_name='+')
315     jingles = models.ManyToManyField(Jingle, blank=True)
316
317     weights_text = models.TextField(null=True)  # will be read as json
318
319     def __str__(self):
320         return str(self.nonstop)
321
322     @property
323     def weights(self):
324         weights = collections.defaultdict(int)
325         for k, v in json.loads(self.weights_text or "{}").items():
326             weights[k] = v
327         return weights
328
329     @weights.setter
330     def weights(self, d):
331         self.weights_text = json.dumps(d)
332
333
334 class RecurringStreamDiffusion(models.Model):
335     schedule = models.ForeignKey('emissions.Schedule', on_delete=models.CASCADE)
336     jingle = models.ForeignKey(Jingle, null=True, blank=True)
337     stream = models.ForeignKey(Stream)
338     is_active = models.BooleanField('Active', default=True)
339
340     def __str__(self):
341         return _('Recurring Stream for %s') % self.schedule
342
343
344 class RecurringOccurenceMixin:
345
346     @property
347     def jingle(self):
348         return self.diffusion.jingle
349
350     @property
351     def jingle_id(self):
352         return self.diffusion.jingle_id
353
354     @property
355     def duration(self):
356         return self.diffusion.schedule.get_duration() * 60
357
358     @property
359     def end_datetime(self):
360         return self.datetime + datetime.timedelta(minutes=self.diffusion.schedule.get_duration())
361
362     def get_jingle_filepath(self):
363         return self.diffusion.jingle.get_local_filepath() if self.diffusion.jingle_id else None
364
365
366 class RecurringStreamOccurence(models.Model, RecurringOccurenceMixin):
367     diffusion = models.ForeignKey(RecurringStreamDiffusion, on_delete=models.CASCADE)
368     datetime = models.DateTimeField(_('Date/time'), db_index=True)
369
370     def __str__(self):
371         return 'Recurring stream of %s' % self.diffusion.stream
372
373     @property
374     def stream(self):
375         return self.diffusion.stream
376
377     def is_stream(self):
378         return True
379
380
381 class RecurringRandomDirectoryDiffusion(models.Model):
382     # between soundfiles and nonstop zones, this is used for the "mix
383     # deliveries" segment during weekend nights.
384     schedule = models.ForeignKey('emissions.Schedule', on_delete=models.CASCADE)
385     jingle = models.ForeignKey(Jingle, null=True, blank=True)
386     directory = models.CharField(_('Directory'), max_length=255)
387     is_active = models.BooleanField('Active', default=True)
388
389     def __str__(self):
390         return _('Recurring Random Directory for %s') % self.schedule
391
392
393 class RecurringRandomDirectoryOccurence(models.Model, RecurringOccurenceMixin):
394     diffusion = models.ForeignKey(RecurringRandomDirectoryDiffusion, on_delete=models.CASCADE)
395     datetime = models.DateTimeField(_('Date/time'), db_index=True)
396
397     def is_stream(self):
398         return False
399
400     def file_path(self):
401         directory = self.diffusion.directory
402         return os.path.join(directory, random.choice(os.listdir(directory)))
403
404
405 @receiver(post_delete)
406 def remove_soundfile(sender, instance=None, **kwargs):
407     from emissions.models import SoundFile
408     if not issubclass(sender, SoundFile):
409         return
410     ScheduledDiffusion.objects.filter(
411             diffusion__episode_id=instance.episode_id,
412             stream_id=None).update(
413             diffusion=None)
414
415
416 @receiver(post_save)
417 def save_soundfile(sender, instance=None, **kwargs):
418     if not app_settings.AUTO_SCHEDULE:
419         return
420     from emissions.models import SoundFile
421     if not issubclass(sender, SoundFile):
422         return
423     if instance.fragment:
424         return
425     if not instance.duration:
426         return
427     for i, diffusion in enumerate(instance.episode.diffusion_set.all()):
428         if i == 0:
429             jingle_qs = Jingle.objects.filter(default_for_initial_diffusions=True)
430         else:
431             jingle_qs = Jingle.objects.filter(default_for_reruns=True)
432         ScheduledDiffusion.objects.get_or_create(
433                 diffusion=diffusion, stream_id=None,
434                 defaults={'jingle': jingle_qs.order_by('?').first()})
435
436
437 @receiver(post_save)
438 def save_diffusion(sender, instance=None, **kwargs):
439     if not app_settings.AUTO_SCHEDULE:
440         return
441     from emissions.models import Diffusion
442     if not issubclass(sender, Diffusion):
443         return
444     if instance.episode.soundfile_set.filter(fragment=False).exists():
445         if Diffusion.objects.filter(episode=instance.episode, datetime__lt=instance.datetime).exists():
446             # rerun
447             jingle_qs = Jingle.objects.filter(default_for_reruns=True)
448         else:
449             jingle_qs = Jingle.objects.filter(default_for_initial_diffusions=True)
450         ScheduledDiffusion.objects.get_or_create(diffusion=instance,
451                 stream_id=None, defaults={'jingle': jingle_qs.order_by('?').first()})