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 _
15 from .app_settings import app_settings
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',
32 class Artist(models.Model):
33 name = models.CharField(_('Name'), max_length=255)
41 def get_absolute_url(self):
42 return reverse('artist-view', kwargs={'pk': self.id})
44 def recent_diffusions(self):
45 return SomaLogLine.objects.filter(filepath__track__artist=self
46 ).exclude(on_air=False).order_by('-play_timestamp')
48 def active_tracks(self):
49 return self.track_set.filter(nonstop_zones__isnull=False).distinct().order_by('title')
51 def available_tracks(self):
52 return self.track_set.filter(nonstop_zones__isnull=True).order_by('title')
55 class Album(models.Model):
56 name = models.CharField(_('Name'), max_length=255)
63 ('other', _('Other')),
64 ('na', _('Not applicable')),
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)
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)
84 ordering = ['creation_timestamp']
87 return 'Track %s (%s)' % (self.title, self.artist or 'unknown')
89 def get_absolute_url(self):
90 return reverse('track-view', kwargs={'pk': self.id})
92 def recent_diffusions(self):
93 return SomaLogLine.objects.filter(filepath__track=self
94 ).exclude(on_air=False).order_by('-play_timestamp')
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()
102 return nfile.get_local_filepath()
105 def file_exists(self):
106 file_path = self.file_path()
110 return os.path.exists(file_path)
111 except AttributeError:
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()
121 self.added_to_nonstop_timestamp = None
124 if not self.file_exists():
126 nonstop_file = self.nonstopfile_set.order_by('creation_timestamp').last()
127 filename = nonstop_file.filename
128 from emissions.models import Nonstop
130 for zone in Nonstop.objects.all():
131 if not zone.slug in TRANCHE_SLUG_DIR_MAPPING:
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)
139 if os.path.exists(zone_path):
142 def match_criteria(self, criteria):
143 return getattr(self, 'match_criteria_' + criteria)()
145 def match_criteria_lang_en(self):
146 return self.language == 'en'
148 def match_criteria_lang_fr(self):
149 return self.language == 'fr'
151 def match_criteria_lang_nl(self):
152 return self.language == 'nl'
154 def match_criteria_lang_other(self):
155 return self.language == 'other'
157 def match_criteria_instru(self):
160 def match_criteria_cfwb(self):
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)
172 return self.filepath[len(app_settings.REMOTE_BASE_PATH):]
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)
178 def get_local_filepath(self):
181 return os.path.join(app_settings.LOCAL_BASE_PATH, self.short)
184 class SomaLogLine(models.Model):
186 verbose_name = _('Soma log line')
187 verbose_name_plural = _('Soma log lines')
188 ordering = ['play_timestamp']
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')
199 return self.filepath.track
203 class Jingle(models.Model):
205 verbose_name = _('Jingle')
206 verbose_name_plural = _('Jingles')
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)
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
227 # for compatibility with Track model method
228 return self.get_local_filepath()
230 def get_local_filepath(self):
233 return os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, self.short)
240 class Stream(models.Model):
242 verbose_name = _('Stream')
243 verbose_name_plural = _('Streams')
246 label = models.CharField(_('Label'), max_length=100)
247 url = models.URLField(_('URL'), max_length=255)
253 class ScheduledDiffusion(models.Model):
255 verbose_name = _('Scheduled diffusion')
256 verbose_name_plural = _('Scheduled diffusions')
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)
266 return 'Diffusion of %s' % self.diffusion
270 return self.diffusion.datetime
273 def end_datetime(self):
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)
284 return self.diffusion.episode.soundfile_set.filter(fragment=False).first()
288 return (self.end_datetime - self.datetime).seconds
292 return self.diffusion.episode
297 return '[stream:%s]' % self.id
299 return '[sound:%s]' % self.id
302 return bool(self.stream_id)
304 def get_jingle_filepath(self):
305 return self.jingle.get_local_filepath() if self.jingle_id else None
308 # TODO, copy and stuff
309 return self.soundfile.file.path
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)
317 weights_text = models.TextField(null=True) # will be read as json
320 return str(self.nonstop)
324 weights = collections.defaultdict(int)
325 for k, v in json.loads(self.weights_text or "{}").items():
330 def weights(self, d):
331 self.weights_text = json.dumps(d)
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)
341 return _('Recurring Stream for %s') % self.schedule
344 class RecurringOccurenceMixin:
348 return self.diffusion.jingle
352 return self.diffusion.jingle_id
356 return self.diffusion.schedule.get_duration() * 60
359 def end_datetime(self):
360 return self.datetime + datetime.timedelta(minutes=self.diffusion.schedule.get_duration())
362 def get_jingle_filepath(self):
363 return self.diffusion.jingle.get_local_filepath() if self.diffusion.jingle_id else None
366 class RecurringStreamOccurence(models.Model, RecurringOccurenceMixin):
367 diffusion = models.ForeignKey(RecurringStreamDiffusion, on_delete=models.CASCADE)
368 datetime = models.DateTimeField(_('Date/time'), db_index=True)
371 return 'Recurring stream of %s' % self.diffusion.stream
375 return self.diffusion.stream
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)
390 return _('Recurring Random Directory for %s') % self.schedule
393 class RecurringRandomDirectoryOccurence(models.Model, RecurringOccurenceMixin):
394 diffusion = models.ForeignKey(RecurringRandomDirectoryDiffusion, on_delete=models.CASCADE)
395 datetime = models.DateTimeField(_('Date/time'), db_index=True)
401 directory = self.diffusion.directory
402 return os.path.join(directory, random.choice(os.listdir(directory)))
405 @receiver(post_delete)
406 def remove_soundfile(sender, instance=None, **kwargs):
407 from emissions.models import SoundFile
408 if not issubclass(sender, SoundFile):
410 ScheduledDiffusion.objects.filter(
411 diffusion__episode_id=instance.episode_id,
412 stream_id=None).update(
417 def save_soundfile(sender, instance=None, **kwargs):
418 if not app_settings.AUTO_SCHEDULE:
420 from emissions.models import SoundFile
421 if not issubclass(sender, SoundFile):
423 if instance.fragment:
425 if not instance.duration:
427 for i, diffusion in enumerate(instance.episode.diffusion_set.all()):
429 jingle_qs = Jingle.objects.filter(default_for_initial_diffusions=True)
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()})
438 def save_diffusion(sender, instance=None, **kwargs):
439 if not app_settings.AUTO_SCHEDULE:
441 from emissions.models import Diffusion
442 if not issubclass(sender, Diffusion):
444 if instance.episode.soundfile_set.filter(fragment=False).exists():
445 if Diffusion.objects.filter(episode=instance.episode, datetime__lt=instance.datetime).exists():
447 jingle_qs = Jingle.objects.filter(default_for_reruns=True)
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()})