+import datetime
import os
+import random
-import mutagen
-
+from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
+from django.db.models.signals import post_delete, post_save
+from django.dispatch import receiver
+from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
-REMOTE_BASE_PATH = '/srv/soma/nonstop/'
-LOCAL_BASE_PATH = '/media/nonstop/'
+from .app_settings import app_settings
+
TRANCHE_SLUG_DIR_MAPPING = {
'acouphene': 'Acouphene',
'up-beat-tempo': 'Up_Beat_Tempo',
}
+
+
class Artist(models.Model):
name = models.CharField(_('Name'), max_length=255)
class Meta:
ordering = ['name']
+ def __str__(self):
+ return self.name
+
def get_absolute_url(self):
return reverse('artist-view', kwargs={'pk': self.id})
return SomaLogLine.objects.filter(filepath__track__artist=self
).exclude(on_air=False).order_by('-play_timestamp')
+ def active_tracks(self):
+ return self.track_set.filter(nonstop_zones__isnull=False).distinct().order_by('title')
+
+ def available_tracks(self):
+ return self.track_set.filter(nonstop_zones__isnull=True).order_by('title')
+
class Album(models.Model):
name = models.CharField(_('Name'), max_length=255)
LANGUAGES = [
('en', _('English')),
('fr', _('French')),
- ('nl', _('Dutch'))
+ ('nl', _('Dutch')),
+ ('other', _('Other')),
+ ('na', _('Not applicable')),
]
class Track(models.Model):
artist = models.ForeignKey(Artist, null=True)
album = models.ForeignKey(Album, null=True)
instru = models.BooleanField(_('Instru'), default=False)
- language = models.CharField(max_length=3,
+ language = models.CharField(max_length=10,
choices=LANGUAGES, blank=True)
sabam = models.BooleanField('SABAM', default=True)
cfwb = models.BooleanField('CFWB', default=False)
nonstop_zones = models.ManyToManyField('emissions.Nonstop', blank=True)
creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
+ added_to_nonstop_timestamp = models.DateTimeField(null=True)
+ uploader = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
+ duration = models.DurationField(_('Duration'), null=True)
class Meta:
ordering = ['creation_timestamp']
+ def __str__(self):
+ return 'Track %s (%s)' % (self.title, self.artist or 'unknown')
+
def get_absolute_url(self):
return reverse('track-view', kwargs={'pk': self.id})
).exclude(on_air=False).order_by('-play_timestamp')
def file_path(self):
- return self.nonstopfile_set.all().order_by('creation_timestamp').last().get_local_filepath()
+ nfile = None
+ for nfile in self.nonstopfile_set.all().order_by('creation_timestamp'):
+ if os.path.exists(nfile.get_local_filepath()):
+ return nfile.get_local_filepath()
+ if nfile:
+ return nfile.get_local_filepath()
+ return None
def file_exists(self):
- return os.path.exists(self.nonstopfile_set.order_by('creation_timestamp').last().get_local_filepath())
+ file_path = self.file_path()
+ if not file_path:
+ return False
+ try:
+ return os.path.exists(file_path)
+ except AttributeError:
+ return False
def sync_nonstop_zones(self):
+ current_zones = self.nonstop_zones.all()
+ if current_zones.count():
+ if not self.added_to_nonstop_timestamp:
+ self.added_to_nonstop_timestamp = now()
+ self.save()
+ else:
+ self.added_to_nonstop_timestamp = None
+ self.save()
+
+ if not self.file_exists():
+ return
nonstop_file = self.nonstopfile_set.order_by('creation_timestamp').last()
filename = nonstop_file.filename
- current_zones = self.nonstop_zones.all()
from emissions.models import Nonstop
for zone in Nonstop.objects.all():
+ if not zone.slug in TRANCHE_SLUG_DIR_MAPPING:
+ continue
zone_dir = TRANCHE_SLUG_DIR_MAPPING[zone.slug]
- zone_path = os.path.join(LOCAL_BASE_PATH, 'Tranches', zone_dir, filename)
+ zone_path = os.path.join(app_settings.LOCAL_BASE_PATH, 'Tranches', zone_dir, filename)
if zone in current_zones:
if not os.path.exists(zone_path):
os.symlink(os.path.join('..', '..', nonstop_file.short), zone_path)
@property
def short(self):
- return self.filepath[len(REMOTE_BASE_PATH):]
+ return self.filepath[len(app_settings.REMOTE_BASE_PATH):]
def set_track_filepath(self, filepath):
- self.filepath = os.path.join(REMOTE_BASE_PATH, 'tracks', filepath)
+ self.filepath = os.path.join(app_settings.REMOTE_BASE_PATH, 'tracks', filepath)
self.filename = os.path.basename(filepath)
def get_local_filepath(self):
- return os.path.join(LOCAL_BASE_PATH, self.short)
+ if not self.short:
+ return None
+ return os.path.join(app_settings.LOCAL_BASE_PATH, self.short)
class SomaLogLine(models.Model):
verbose_name_plural = _('Soma log lines')
ordering = ['play_timestamp']
- filepath = models.ForeignKey(NonstopFile)
+ filepath = models.ForeignKey(NonstopFile, null=True)
+ track = models.ForeignKey(Track, null=True)
play_timestamp = models.DateTimeField()
on_air = models.NullBooleanField('On Air')
+
+ def get_track(self):
+ if self.track_id:
+ return self.track
+ if self.filepath_id:
+ return self.filepath.track
+ return None
+
+
+class Jingle(models.Model):
+ class Meta:
+ verbose_name = _('Jingle')
+ verbose_name_plural = _('Jingles')
+ ordering = ['label']
+
+ label = models.CharField(_('Label'), max_length=100)
+ filepath = models.CharField(_('File Path'), max_length=255)
+ duration = models.DurationField(_('Duration'), null=True, blank=True)
+ default_for_initial_diffusions = models.BooleanField(_('Default for initial diffusions'), default=False)
+ default_for_reruns = models.BooleanField(_('Default for reruns'), default=False)
+ default_for_streams = models.BooleanField(_('Default for streams'), default=False)
+
+ def __str__(self):
+ return self.label
+
+ @property
+ def short(self):
+ # property for compatibility with Track model
+ # for jingles self.filepath is actually only the last part of the path,
+ # ex: jingles panik/H_marimba_RP_chucho_zoe.wav
+ return self.filepath
+
+ def file_path(self):
+ # for compatibility with Track model method
+ return self.get_local_filepath()
+
+ def get_local_filepath(self):
+ if not self.short:
+ return None
+ return os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, self.short)
+
+ @property
+ def title(self):
+ return self.label
+
+
+class Stream(models.Model):
+ class Meta:
+ verbose_name = _('Stream')
+ verbose_name_plural = _('Streams')
+ ordering = ['label']
+
+ label = models.CharField(_('Label'), max_length=100)
+ url = models.URLField(_('URL'), max_length=255)
+
+ def __str__(self):
+ return self.label
+
+
+class ScheduledDiffusion(models.Model):
+ class Meta:
+ verbose_name = _('Scheduled diffusion')
+ verbose_name_plural = _('Scheduled diffusions')
+
+ diffusion = models.ForeignKey('emissions.Diffusion', null=True, blank=True, on_delete=models.SET_NULL)
+ jingle = models.ForeignKey(Jingle, null=True, blank=True)
+ stream = models.ForeignKey(Stream, null=True, blank=True)
+ creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
+ added_to_nonstop_timestamp = models.DateTimeField(null=True)
+ auto_delayed = models.BooleanField(default=False)
+
+ def __str__(self):
+ return 'Diffusion of %s' % self.diffusion
+
+ @property
+ def datetime(self):
+ return self.diffusion.datetime
+
+ @property
+ def end_datetime(self):
+ if self.is_stream():
+ return self.diffusion.end_datetime
+ dt = self.diffusion.datetime
+ if self.jingle and self.jingle.duration:
+ dt += self.jingle.duration
+ dt += datetime.timedelta(seconds=self.soundfile.duration)
+ return dt
+
+ @property
+ def soundfile(self):
+ return self.diffusion.episode.soundfile_set.filter(fragment=False).first()
+
+ @property
+ def duration(self):
+ return (self.end_datetime - self.datetime).seconds
+
+ @property
+ def episode(self):
+ return self.diffusion.episode
+
+ @property
+ def soma_id(self):
+ if self.is_stream():
+ return '[stream:%s]' % self.id
+ else:
+ return '[sound:%s]' % self.id
+
+ def is_stream(self):
+ return bool(self.stream_id)
+
+ def get_jingle_filepath(self):
+ return self.jingle.get_local_filepath() if self.jingle_id else None
+
+ def file_path(self):
+ # TODO, copy and stuff
+ return self.soundfile.file.path
+
+
+class NonstopZoneSettings(models.Model):
+ nonstop = models.ForeignKey('emissions.Nonstop', on_delete=models.CASCADE)
+ intro_jingle = models.ForeignKey(Jingle, blank=True, null=True, related_name='+')
+ jingles = models.ManyToManyField(Jingle, blank=True)
+
+ def __str__(self):
+ return str(self.nonstop)
+
+
+class RecurringStreamDiffusion(models.Model):
+ schedule = models.ForeignKey('emissions.Schedule', on_delete=models.CASCADE)
+ jingle = models.ForeignKey(Jingle, null=True, blank=True)
+ stream = models.ForeignKey(Stream)
+ is_active = models.BooleanField('Active', default=True)
+
+ def __str__(self):
+ return _('Recurring Stream for %s') % self.schedule
+
+
+class RecurringOccurenceMixin:
+
+ @property
+ def jingle(self):
+ return self.diffusion.jingle
+
+ @property
+ def jingle_id(self):
+ return self.diffusion.jingle_id
+
+ @property
+ def duration(self):
+ return self.diffusion.schedule.get_duration() * 60
+
+ @property
+ def end_datetime(self):
+ return self.datetime + datetime.timedelta(minutes=self.diffusion.schedule.get_duration())
+
+ def get_jingle_filepath(self):
+ return self.diffusion.jingle.get_local_filepath() if self.diffusion.jingle_id else None
+
+
+class RecurringStreamOccurence(models.Model, RecurringOccurenceMixin):
+ diffusion = models.ForeignKey(RecurringStreamDiffusion, on_delete=models.CASCADE)
+ datetime = models.DateTimeField(_('Date/time'), db_index=True)
+
+ def __str__(self):
+ return 'Recurring stream of %s' % self.diffusion.stream
+
+ @property
+ def stream(self):
+ return self.diffusion.stream
+
+ def is_stream(self):
+ return True
+
+
+class RecurringRandomDirectoryDiffusion(models.Model):
+ # between soundfiles and nonstop zones, this is used for the "mix
+ # deliveries" segment during weekend nights.
+ schedule = models.ForeignKey('emissions.Schedule', on_delete=models.CASCADE)
+ jingle = models.ForeignKey(Jingle, null=True, blank=True)
+ directory = models.CharField(_('Directory'), max_length=255)
+ is_active = models.BooleanField('Active', default=True)
+
+ def __str__(self):
+ return _('Recurring Random Directory for %s') % self.schedule
+
+
+class RecurringRandomDirectoryOccurence(models.Model, RecurringOccurenceMixin):
+ diffusion = models.ForeignKey(RecurringRandomDirectoryDiffusion, on_delete=models.CASCADE)
+ datetime = models.DateTimeField(_('Date/time'), db_index=True)
+
+ def is_stream(self):
+ return False
+
+ def file_path(self):
+ directory = self.diffusion.directory
+ return os.path.join(directory, random.choice(os.listdir(directory)))
+
+
+@receiver(post_delete)
+def remove_soundfile(sender, instance=None, **kwargs):
+ from emissions.models import SoundFile
+ if not issubclass(sender, SoundFile):
+ return
+ ScheduledDiffusion.objects.filter(
+ diffusion__episode_id=instance.episode_id,
+ stream_id=None).update(
+ diffusion=None)
+
+
+@receiver(post_save)
+def save_soundfile(sender, instance=None, **kwargs):
+ if not app_settings.AUTO_SCHEDULE:
+ return
+ from emissions.models import SoundFile
+ if not issubclass(sender, SoundFile):
+ return
+ if instance.fragment:
+ return
+ if not instance.duration:
+ return
+ for i, diffusion in enumerate(instance.episode.diffusion_set.all()):
+ if i == 0:
+ jingle_qs = Jingle.objects.filter(default_for_initial_diffusions=True)
+ else:
+ jingle_qs = Jingle.objects.filter(default_for_reruns=True)
+ ScheduledDiffusion.objects.get_or_create(
+ diffusion=diffusion, stream_id=None,
+ defaults={'jingle': jingle_qs.order_by('?').first()})
+
+
+@receiver(post_save)
+def save_diffusion(sender, instance=None, **kwargs):
+ if not app_settings.AUTO_SCHEDULE:
+ return
+ from emissions.models import Diffusion
+ if not issubclass(sender, Diffusion):
+ return
+ if instance.episode.soundfile_set.filter(fragment=False).exists():
+ if Diffusion.objects.filter(episode=instance.episode, datetime__lt=instance.datetime).exists():
+ # rerun
+ jingle_qs = Jingle.objects.filter(default_for_reruns=True)
+ else:
+ jingle_qs = Jingle.objects.filter(default_for_initial_diffusions=True)
+ ScheduledDiffusion.objects.get_or_create(diffusion=instance,
+ stream_id=None, defaults={'jingle': jingle_qs.order_by('?').first()})