redesign streams, make liquidsoap working with timed streams

This commit is contained in:
bkfox 2015-11-05 16:59:28 +01:00
parent bd987bd62c
commit 758bcb30a2
4 changed files with 73 additions and 87 deletions

View File

@ -11,6 +11,7 @@ from django.views.generic.base import View
from django.template.loader import render_to_string from django.template.loader import render_to_string
import aircox_liquidsoap.settings as settings import aircox_liquidsoap.settings as settings
import aircox_programs.settings as programs_settings
import aircox_programs.models as models import aircox_programs.models as models
@ -51,7 +52,8 @@ class Command (BaseCommand):
if options.get('stream'): if options.get('stream'):
stream = options['stream'] stream = options['stream']
if type(stream) is int: if type(stream) is int:
stream = models.Stream.objects.get(id = stream) stream = models.Stream.objects.get(id = stream,
program__active = True)
data = self.get_playlist(stream, output = output) data = self.get_playlist(stream, output = output)
return return
@ -65,7 +67,7 @@ class Command (BaseCommand):
if options.get('all'): if options.get('all'):
self.handle(config = True) self.handle(config = True)
for stream in models.Stream.objects.filter(active = True): for stream in models.Stream.objects.filter(program__active = True):
self.handle(stream = stream) self.handle(stream = stream)
self.output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA self.output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA
return return
@ -85,13 +87,13 @@ class Command (BaseCommand):
@staticmethod @staticmethod
def __render_stream_in_radio (stream): def __render_stream_in_radio (stream):
if stream.time_start and stream.time_end: if stream.time_start and stream.time_end:
data = '({}-{}, {})'.format( data = '({{{}h-{}h}}, {})'.format(
stream.time_start.strftime('%Hh%M'), stream.time_start.hour,
stream.time_end.strftime('%Hh%M'), stream.time_end.hour,
stream.get_slug_name() stream.program.get_slug_name()
) )
else: else:
data = stream.get_slug_name() data = stream.program.get_slug_name()
if stream.delay: if stream.delay:
data = 'delay({}., {})'.format( data = 'delay({}., {})'.format(
@ -101,7 +103,7 @@ class Command (BaseCommand):
return data return data
def get_config (self, output = None): def get_config (self, output = None):
streams = models.Stream.objects.filter(active = True).order_by('type')[:] streams = models.Stream.objects.filter(program__active = True)
for stream in streams: for stream in streams:
stream.render_in_radio = self.__render_stream_in_radio(stream) stream.render_in_radio = self.__render_stream_in_radio(stream)
@ -111,15 +113,22 @@ class Command (BaseCommand):
} }
data = render_to_string('aircox_liquidsoap/config.liq', context) data = render_to_string('aircox_liquidsoap/config.liq', context)
data = re.sub(r'\\\n', r'#\\n#', data) data = re.sub(r'\s*\\\n', r'#\\n#', data)
data = data.replace('\n', '') data = data.replace('\n', '')
data = re.sub(r'#\\n#', '\n', data) data = re.sub(r'#\\n#', '\n', data)
self.print(data, output, 'aircox.liq') self.print(data, output, 'aircox.liq')
def get_playlist (self, stream, output = None): def get_playlist (self, stream, output = None):
data = '/media/data/musique/free/Professor Kliq -- 28 Days With The OP-1' \ path = os.path.join(
'-- jm148689/1_Coffee.ogg\n' programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
stream.program.path
)
sounds = models.Sound.objects.filter(
# good_quality = True,
type = models.Sound.Type['archive'],
path__startswith = path
)
data = '\n'.join(sound.path for sound in sounds)
self.print(data, output, 'stream_{}.m3u'.format(stream.pk)) self.print(data, output, 'stream_{}.m3u'.format(stream.pk))

View File

@ -26,8 +26,8 @@ class ScheduleInline (admin.TabularInline):
class DiffusionInline (admin.TabularInline): class DiffusionInline (admin.TabularInline):
model = Diffusion model = Diffusion
fields = ('episode', 'type', 'date', 'stream') fields = ('episode', 'type', 'date')
readonly_fields = ('date', 'stream') readonly_fields = ('date',)
extra = 1 extra = 1
@ -59,14 +59,13 @@ class SoundAdmin (NameableAdmin):
@admin.register(Stream) @admin.register(Stream)
class StreamAdmin (SortableModelAdmin): class StreamAdmin (admin.ModelAdmin):
list_display = ('id', 'name', 'type') list_display = ('id', 'program', 'delay', 'time_start', 'time_end')
sortable = "priority"
@admin.register(Program) @admin.register(Program)
class ProgramAdmin (NameableAdmin): class ProgramAdmin (NameableAdmin):
fields = NameableAdmin.fields + ['stream'] fields = NameableAdmin.fields
inlines = [ ScheduleInline ] inlines = [ ScheduleInline ]
@ -86,8 +85,8 @@ class DiffusionAdmin (admin.ModelAdmin):
if sound.type == Sound.Type['archive'] ) if sound.type == Sound.Type['archive'] )
return ', '.join(sounds) if sounds else '' return ', '.join(sounds) if sounds else ''
list_display = ('id', 'type', 'date', 'archives', 'episode', 'program', 'stream') list_display = ('id', 'type', 'date', 'archives', 'episode', 'program')
list_filter = ('type', 'date', 'program', 'stream') list_filter = ('type', 'date', 'program')
list_editable = ('type', 'date') list_editable = ('type', 'date')
def get_queryset(self, request): def get_queryset(self, request):

View File

@ -59,22 +59,24 @@ class Command (BaseCommand):
if options.get('quality_check'): if options.get('quality_check'):
self.check_quality(check = (not options.get('scan')) ) self.check_quality(check = (not options.get('scan')) )
def get_sound_info (self, path): def get_sound_info (self, program, path):
""" """
Parse file name to get info on the assumption it has the correct Parse file name to get info on the assumption it has the correct
format (given in Command.help) format (given in Command.help)
""" """
file_name = os.path.basename(path)
file_name = os.path.splitext(file_name)[0]
r = re.search('^(?P<year>[0-9]{4})' r = re.search('^(?P<year>[0-9]{4})'
'(?P<month>[0-9]{2})' '(?P<month>[0-9]{2})'
'(?P<day>[0-9]{2})' '(?P<day>[0-9]{2})'
'(_(?P<n>[0-9]+))?' '(_(?P<n>[0-9]+))?'
'_?(?P<name>.*)\.\w+$', '_?(?P<name>.*)$',
os.path.basename(path)) file_name)
if not (r and r.groupdict()): if not (r and r.groupdict()):
self.report(program, path, "file path is not correct, use defaults") self.report(program, path, "file path is not correct, use defaults")
r = { r = {
'name': os.path.splitext(path)[0] 'name': file_name
} }
else: else:
r = r.groupdict() r = r.groupdict()
@ -143,7 +145,7 @@ class Command (BaseCommand):
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT): if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
continue continue
sound_info = self.get_sound_info(path) sound_info = self.get_sound_info(program, path)
sound = Sound.objects.get_or_create( sound = Sound.objects.get_or_create(
path = path, path = path,
defaults = { 'name': sound_info['name'] } defaults = { 'name': sound_info['name'] }

View File

@ -31,7 +31,7 @@ class Nameable (models.Model):
) )
def get_slug_name (self): def get_slug_name (self):
return slugify(self.name) return slugify(self.name).replace('-', '_')
def __str__ (self): def __str__ (self):
#if self.pk: #if self.pk:
@ -137,6 +137,8 @@ class Sound (Nameable):
""" """
mtime = os.stat(self.path).st_mtime mtime = os.stat(self.path).st_mtime
mtime = tz.datetime.fromtimestamp(mtime) mtime = tz.datetime.fromtimestamp(mtime)
# db does not store microseconds
mtime = mtime.replace(microsecond = 0)
return tz.make_aware(mtime, tz.get_current_timezone()) return tz.make_aware(mtime, tz.get_current_timezone())
def file_exists (self): def file_exists (self):
@ -181,6 +183,38 @@ class Sound (Nameable):
verbose_name_plural = _('Sounds') verbose_name_plural = _('Sounds')
class Stream (models.Model):
"""
When there are no program scheduled, it is possible to play sounds
in order to avoid blanks. A Stream is a Program that plays this role,
and whose linked to a Stream.
All sounds that are marked as good and that are under the related
program's archive dir are elligible for the sound's selection.
"""
program = models.ForeignKey(
'Program',
verbose_name = _('related program'),
)
delay = models.TimeField(
_('delay'),
blank = True, null = True,
help_text = _('plays this playlist at least every delay')
)
time_start = models.TimeField(
_('start'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
time_end = models.TimeField(
_('end'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
class Schedule (models.Model): class Schedule (models.Model):
# Frequency for schedules. Basically, it is a mask of bits where each bit is # Frequency for schedules. Basically, it is a mask of bits where each bit is
# a week. Bits > rank 5 are used for special schedules. # a week. Bits > rank 5 are used for special schedules.
@ -203,7 +237,7 @@ class Schedule (models.Model):
program = models.ForeignKey( program = models.ForeignKey(
'Program', 'Program',
blank = True, null = True, verbose_name = _('related program'),
) )
date = models.DateTimeField(_('date')) date = models.DateTimeField(_('date'))
duration = models.TimeField( duration = models.TimeField(
@ -327,7 +361,6 @@ class Schedule (models.Model):
diffusions.append(Diffusion( diffusions.append(Diffusion(
episode = episode, episode = episode,
program = self.program, program = self.program,
stream = self.program.stream,
type = Diffusion.Type['unconfirmed'], type = Diffusion.Type['unconfirmed'],
date = date, date = date,
)) ))
@ -363,13 +396,6 @@ class Diffusion (models.Model):
'Program', 'Program',
verbose_name = _('program'), verbose_name = _('program'),
) )
# program.stream can change, but not the stream;
stream = models.ForeignKey(
'Stream',
verbose_name = _('stream'),
default = 0,
help_text = 'stream id on which the diffusion happens',
)
type = models.SmallIntegerField( type = models.SmallIntegerField(
verbose_name = _('type'), verbose_name = _('type'),
choices = [ (y, x) for x,y in Type.items() ], choices = [ (y, x) for x,y in Type.items() ],
@ -379,7 +405,6 @@ class Diffusion (models.Model):
def save (self, *args, **kwargs): def save (self, *args, **kwargs):
if self.episode: # FIXME self.episode or kwargs['episode'] if self.episode: # FIXME self.episode or kwargs['episode']
self.program = self.episode.program self.program = self.episode.program
# check type against stream's type
super(Diffusion, self).save(*args, **kwargs) super(Diffusion, self).save(*args, **kwargs)
def __str__ (self): def __str__ (self):
@ -391,56 +416,7 @@ class Diffusion (models.Model):
verbose_name_plural = _('Diffusions') verbose_name_plural = _('Diffusions')
class Stream (Nameable):
Type = {
'random': 0x00, # selection using random function
'schedule': 0x01, # selection using schedule
}
for key, value in Type.items():
ugettext_lazy(key)
public = models.BooleanField(
_('public'),
default = True,
help_text = _('program list is public'),
)
active = models.BooleanField(
_('active'),
default = True,
help_text = _('stream is active')
)
type = models.SmallIntegerField(
verbose_name = _('type'),
choices = [ (y, x) for x,y in Type.items() ],
)
delay = models.TimeField(
_('delay'),
blank = True, null = True,
help_text = _('play this playlist at least every delay')
)
time_start = models.TimeField(
_('start'),
blank = True, null = True,
help_text = _('if random, used to define a time range this stream is'
'played')
)
time_end = models.TimeField(
_('end'),
blank = True, null = True,
help_text = _('if random, used to define a time range this stream is'
'played')
)
# get info for:
# - random lists
# - scheduled lists
class Program (Nameable): class Program (Nameable):
stream = models.ForeignKey(
Stream,
verbose_name = _('streams'),
)
active = models.BooleanField( active = models.BooleanField(
_('inactive'), _('inactive'),
default = True, default = True,
@ -450,7 +426,7 @@ class Program (Nameable):
@property @property
def path (self): def path (self):
return os.path.join(settings.AIRCOX_PROGRAMS_DIR, return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
slugify(self.name + '_' + str(self.id)) ) self.get_slug_name() + '_' + str(self.id) )
def ensure_dir (self, subdir = None): def ensure_dir (self, subdir = None):
""" """
@ -477,12 +453,12 @@ class Program (Nameable):
if schedule.match(date, check_time = False): if schedule.match(date, check_time = False):
return schedule return schedule
class Episode (Nameable): class Episode (Nameable):
program = models.ForeignKey( program = models.ForeignKey(
Program, Program,
verbose_name = _('program'), verbose_name = _('program'),
help_text = _('parent program'), help_text = _('parent program'),
blank = True, null = True,
) )
sounds = models.ManyToManyField( sounds = models.ManyToManyField(
Sound, Sound,