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