This commit is contained in:
bkfox 2015-12-22 18:16:07 +01:00
parent 9cc6c2e248
commit 3e038c4c16
8 changed files with 195 additions and 108 deletions

View File

@ -106,15 +106,15 @@
</header> </header>
<div class="sources"> <div class="sources">
{% with source=controller.master %} {% with source=controller.master %}
{% include 'aircox_liquidsoap/source.html' %} {% include 'aircox/liquidsoap/source.html' %}
{% endwith %} {% endwith %}
{% with source=controller.dealer %} {% with source=controller.dealer %}
{% include 'aircox_liquidsoap/source.html' %} {% include 'aircox/liquidsoap/source.html' %}
{% endwith %} {% endwith %}
{% for source in controller.streams.values %} {% for source in controller.streams.values %}
{% include 'aircox_liquidsoap/source.html' %} {% include 'aircox/liquidsoap/source.html' %}
{% endfor %} {% endfor %}
</div> </div>
<div class="next"> <div class="next">

View File

@ -4,7 +4,8 @@
def interactive_source (id, s) = \ def interactive_source (id, s) = \
def handler(m) = \ def handler(m) = \
file = string.escape(m['filename']) \ file = string.escape(m['filename']) \
system('{{ log_script }} -s "#{id}" -p "#{file}" -c "liquidsoap: play" &') \ system('echo {{ log_script }} -s "#{id}" -p \"#{file}\" -c "liquidsoap: play" &') \
system('{{ log_script }} -s "#{id}" -p \"#{file}\" -c "liquidsoap: play" &') \
end \ end \
\ \
s = on_track(id=id, handler, s) s = on_track(id=id, handler, s)
@ -65,7 +66,7 @@ set("{{ key|safe }}", {{ value|safe }}) \
{% if controller.station.fallback %} {% if controller.station.fallback %}
single("{{ controller.station.fallback }}"), \ single("{{ controller.station.fallback }}"), \
{% else %} {% else %}
blank(), \ blank(duration=0.1), \
{% endif %} {% endif %}
]) \ ]) \
) \ ) \

View File

@ -173,7 +173,7 @@ class Source:
def stream_info (self): def stream_info (self):
""" """
Return a dict with info related to the program's stream Return a dict with info related to the program's stream.
""" """
if not self.program: if not self.program:
return return
@ -279,6 +279,38 @@ class Dealer (Source):
if diffusion.playlist and on_air not in diffusion.playlist: if diffusion.playlist and on_air not in diffusion.playlist:
return diffusion return diffusion
def monitor (self):
"""
Monitor playlist (if it is time to load) and if it time to trigger
the button to start a diffusion.
"""
playlist = self.playlist
on_air = self.current_sound
now = tz.make_aware(tz.datetime.now())
diff = self.__get_next(now, on_air)
if not diff:
return # there is nothing we can do
# playlist reload
if self.playlist != diff.playlist:
if not playlist or on_air == playlist[-1] or \
on_air not in playlist:
self.on = False
self.playlist = diff.playlist
# run the diff
if self.playlist == diff.playlist and diff.date <= now and not self.on:
self.on = True
for source in self.controller.streams.values():
source.skip()
self.controller.log(
source = self.id,
date = now,
comment = 'trigger the scheduled diffusion to liquidsoap; '
'skip all other streams',
related_object = diff,
)
class Controller: class Controller:
""" """
@ -385,7 +417,6 @@ class Controller:
""" """
if not self.connector.available and self.connector.open(): if not self.connector.available and self.connector.open():
return return
self.dealer.monitor() self.dealer.monitor()

View File

@ -3,6 +3,7 @@ import copy
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.db import models from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from aircox.programs.models import * from aircox.programs.models import *
@ -64,6 +65,12 @@ class StationAdmin (NameableAdmin):
@admin.register(Program) @admin.register(Program)
class ProgramAdmin (NameableAdmin): class ProgramAdmin (NameableAdmin):
def schedule (self, obj):
return Schedule.objects.filter(program = obj).count() > 0
schedule.boolean = True
schedule.short_description = _("Schedule")
list_display = ('id', 'name', 'active', 'schedule')
fields = NameableAdmin.fields + [ 'station', 'active' ] fields = NameableAdmin.fields + [ 'station', 'active' ]
# TODO list_display # TODO list_display
inlines = [ ScheduleInline, StreamInline ] inlines = [ ScheduleInline, StreamInline ]
@ -92,7 +99,7 @@ class DiffusionAdmin (admin.ModelAdmin):
list_filter = ('type', 'date', 'program') list_filter = ('type', 'date', 'program')
list_editable = ('type', 'date') list_editable = ('type', 'date')
fields = ['type', 'date', 'initial', 'program', 'sounds'] fields = ['type', 'date', 'duration', 'initial', 'program', 'sounds']
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
if request.user.has_perm('aircox_program.programming'): if request.user.has_perm('aircox_program.programming'):
@ -118,6 +125,24 @@ class LogAdmin (admin.ModelAdmin):
list_display = ['id', 'date', 'source', 'comment', 'related_object'] list_display = ['id', 'date', 'source', 'comment', 'related_object']
list_filter = ['date', 'related_type'] list_filter = ['date', 'related_type']
admin.site.register(Track)
admin.site.register(Schedule) @admin.register(Schedule)
class ScheduleAdmin (admin.ModelAdmin):
def program_name (self, obj):
return obj.program.name
program_name.short_description = _('Program')
def day (self, obj):
return obj.date.strftime('%A')
day.short_description = _('Day')
def rerun (self, obj):
return obj.initial != None
rerun.short_description = _('Rerun')
rerun.boolean = True
list_display = ['id', 'program_name', 'frequency', 'date', 'day', 'rerun']
list_editable = ['frequency', 'date']
admin.site.register(Track)

View File

@ -23,7 +23,7 @@ from django.utils import timezone as tz
from aircox.programs.models import * from aircox.programs.models import *
logger = logging.getLogger('aircox.programs.' + __name__) logger = logging.getLogger('aircox.tools')
class Actions: class Actions:
@staticmethod @staticmethod
@ -33,19 +33,28 @@ class Actions:
items if they have been generated during this items if they have been generated during this
update. update.
It set an attribute 'do_not_save' if the item should not
be saved. FIXME: find proper way
Return the number of conflicts Return the number of conflicts
""" """
conflicts = item.get_conflicts() conflicts = item.get_conflicts()
for i, conflict in enumerate(conflicts):
if conflict.program == item.program:
item.do_not_save = True
del conflicts[i]
continue
if conflict.pk in saved_items and \
conflict.type != Diffusion.Type['unconfirmed']:
conflict.type = Diffusion.Type['unconfirmed']
conflict.save()
if not conflicts: if not conflicts:
item.type = Diffusion.Type['normal'] item.type = Diffusion.Type['normal']
return 0 return 0
item.type = Diffusion.Type['unconfirmed'] item.type = Diffusion.Type['unconfirmed']
for conflict in conflicts:
if conflict.pk in saved_items and \
conflict.type != Diffusion.Type['unconfirmed']:
conflict.type = Diffusion.Type['unconfirmed']
conflict.save()
return len(conflicts) return len(conflicts)
@classmethod @classmethod
@ -67,14 +76,18 @@ class Actions:
else: else:
for item in items: for item in items:
count[1] += cl.__check_conflicts(item, saved_items) count[1] += cl.__check_conflicts(item, saved_items)
if hasattr(item, 'do_not_save'):
count[0] -= 1
continue
item.save() item.save()
saved_items.add(item) saved_items.add(item)
logger.info('[update] {} new diffusions for schedule #{} ({})'.format( logger.info('[update] schedule %s: %d new diffusions',
len(items), schedule.id, str(schedule) str(schedule), len(items),
)) )
logger.info('[update] total of {} diffusions have been created,'.format(count[0]), logger.info('[update] %d diffusions have been created, %s', count[0],
'do not forget manual approval' if manual else 'do not forget manual approval' if manual else
'{} conflicts found'.format(count[1])) '{} conflicts found'.format(count[1]))
@ -82,7 +95,7 @@ class Actions:
def clean (date): def clean (date):
qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'], qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'],
date__lt = date) date__lt = date)
logger.info('[clean] {} diffusions will be removed'.format(qs.count())) logger.info('[clean] %d diffusions will be removed', qs.count())
qs.delete() qs.delete()
@staticmethod @staticmethod
@ -98,7 +111,7 @@ class Actions:
else: else:
items.append(diffusion.id) items.append(diffusion.id)
logger.info('[check] {} diffusions will be removed'.format(len(items))) logger.info('[check] %d diffusions will be removed', len(items))
if len(items): if len(items):
Diffusion.objects.filter(id__in = items).delete() Diffusion.objects.filter(id__in = items).delete()

View File

@ -33,16 +33,17 @@ from aircox.programs.models import *
import aircox.programs.settings as settings import aircox.programs.settings as settings
import aircox.programs.utils as utils import aircox.programs.utils as utils
logger = logging.getLogger('aircox.programs.' + __name__) logger = logging.getLogger('aircox.tools')
class Command (BaseCommand): class Command (BaseCommand):
help= __doc__ help= __doc__
def report (self, program = None, component = None, *content): def report (self, program = None, component = None, *content):
if not component: if not component:
logger.info('{}: '.format(program), *content) logger.info('%s: %s', str(program), ' '.join([str(c) for c in content]))
else: else:
logger.info('{}, {}: '.format(program, component), *content) logger.info('%s, %s: %s', str(program), str(component),
' '.join([str(c) for c in content]))
def add_arguments (self, parser): def add_arguments (self, parser):
parser.formatter_class=RawTextHelpFormatter parser.formatter_class=RawTextHelpFormatter
@ -70,7 +71,7 @@ class Command (BaseCommand):
if not err: if not err:
return utils.seconds_to_time(int(float(out))) return utils.seconds_to_time(int(float(out)))
def get_sound_info (self, program, 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)
@ -92,7 +93,6 @@ class Command (BaseCommand):
else: else:
r = r.groupdict() r = r.groupdict()
r['duration'] = self._get_duration(path)
r['name'] = r['name'].replace('_', ' ').capitalize() r['name'] = r['name'].replace('_', ' ').capitalize()
r['path'] = path r['path'] = path
return r return r
@ -132,11 +132,11 @@ class Command (BaseCommand):
""" """
For all programs, scan dirs For all programs, scan dirs
""" """
logger.info('scan files for all programs...') logger.info('scan all programs...')
programs = Program.objects.filter() programs = Program.objects.filter()
for program in programs: for program in programs:
logger.info('program', program.name) logger.info('#%d %s', program.id, program.name)
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'], type = Sound.Type['archive'],
@ -151,7 +151,7 @@ class Command (BaseCommand):
Scan a given directory that is associated to the given program, and Scan a given directory that is associated to the given program, and
update sounds information. update sounds information.
""" """
logger.info('scan files in', subdir) logger.info('- %s/', subdir)
if not program.ensure_dir(subdir): if not program.ensure_dir(subdir):
return return
@ -163,14 +163,17 @@ 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(program, path) sound, created = Sound.objects.get_or_create(
sound = Sound.objects.get_or_create(
path = path, path = path,
defaults = { 'name': sound_info['name'], defaults = sound_kwargs,
'duration': sound_info['duration'] or None } )
)[0]
sound.__dict__.update(sound_kwargs) sound_info = self._get_sound_info(program, path)
sound.save(check = False)
if created or sound.check_on_file():
sound_info['duration'] = self._get_duration()
sound.__dict__.update(sound_info)
sound.save(check = False)
# initial diffusion association # initial diffusion association
if 'year' in sound_info: if 'year' in sound_info:
@ -181,12 +184,12 @@ class Command (BaseCommand):
self.report(program, path, self.report(program, path,
'the diffusion must be an initial diffusion') 'the diffusion must be an initial diffusion')
else: else:
sound = initial.sounds.get_queryset() \ sound_ = initial.sounds.get_queryset() \
.filter(path == sound.path) .filter(path = sound.path)
if not sound: if not sound_:
self.report(program, path, self.report(program, path,
'add sound to diffusion ', initial.id) 'add sound to diffusion ', initial.id)
initial.sounds.add(sound) initial.sounds.add(sound.pk)
initial.save() initial.save()
self.check_sounds(Sound.objects.filter(path__startswith = subdir)) self.check_sounds(Sound.objects.filter(path__startswith = subdir))
@ -201,16 +204,18 @@ class Command (BaseCommand):
sounds = Sound.objects.filter(good_quality = False) sounds = Sound.objects.filter(good_quality = False)
if check: if check:
self.check_sounds(sounds) self.check_sounds(sounds)
files = [ sound.path for sound in sounds if not sound.removed ] files = [ sound.path for sound in sounds
if not sound.removed and os.path.exists(sound.path) ]
else: else:
files = [ sound.path for sound in sounds.filter(removed = False) ] files = [ sound.path for sound in sounds.filter(removed = False)
if os.path.exists(sound.path) ]
logger.info('start quality check...') logger.info('quality check...',)
cmd = quality_check.Command() cmd = quality_check.Command()
cmd.handle( files = files, cmd.handle( files = files,
**settings.AIRCOX_SOUND_QUALITY ) **settings.AIRCOX_SOUND_QUALITY )
logger.info('update sounds in database') logger.info('update database')
def update_stats(sound_info, sound): def update_stats(sound_info, sound):
stats = sound_info.get_file_stats() stats = sound_info.get_file_stats()
if stats: if stats:

View File

@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
logger = logging.getLogger('aircox.programs.' + __name__) logger = logging.getLogger('aircox.tools')
class Stats: class Stats:
attributes = [ attributes = [
@ -104,11 +104,11 @@ class Sound:
] ]
if self.good: if self.good:
logger.info(self.path, ': good samples:\033[92m', logger.info(self.path + ': good samples:\033[92m%s\033[0m',
', '.join( view(self.good) ), '\033[0m') ', '.join(view(self.good)))
if self.bad: if self.bad:
loggeer.info(self.path ': bad samples:\033[91m', logger.info(self.path + ': good samples:\033[91m%s\033[0m',
', '.join( view(self.bad) ), '\033[0m') ', '.join(view(self.bad)))
class Command (BaseCommand): class Command (BaseCommand):
help = __doc__ help = __doc__
@ -157,7 +157,7 @@ class Command (BaseCommand):
self.bad = [] self.bad = []
self.good = [] self.good = []
for sound in self.sounds: for sound in self.sounds:
logger.info('analyse ', sound.path) logger.info('analyse ' + sound.path)
sound.analyse() sound.analyse()
sound.check(attr, minmax[0], minmax[1]) sound.check(attr, minmax[0], minmax[1])
if sound.bad: if sound.bad:
@ -167,13 +167,8 @@ class Command (BaseCommand):
# resume # resume
if options.get('resume'): if options.get('resume'):
if self.good: for sound in self.good:
logger.info('files that did not failed the test:\033[92m\n ', logger.info('\033[92m+ %s\033[0m', sound.path)
'\n '.join([sound.path for sound in self.good]), for sound in self.bad:
'\033[0m') logger.info('\033[91m+ %s\033[0m', sound.path)
if self.bad:
# bad at the end for ergonomy
logger.info('files that failed the test:\033[91m\n ',
'\n '.join([sound.path for sound in self.bad]),
'\033[0m')

View File

@ -16,7 +16,7 @@ import aircox.programs.utils as utils
import aircox.programs.settings as settings import aircox.programs.settings as settings
logger = logging.getLogger(__name__) logger = logging.getLogger('aircox.core')
def date_or_default (date, date_only = False): def date_or_default (date, date_only = False):
@ -171,7 +171,7 @@ class Sound (Nameable):
if not self.file_exists(): if not self.file_exists():
if self.removed: if self.removed:
return return
logger.info('sound file {} has been removed'.format(self.path)) logger.info('sound %s: has been removed', self.path)
self.removed = True self.removed = True
return True return True
@ -182,8 +182,8 @@ class Sound (Nameable):
if self.mtime != mtime: if self.mtime != mtime:
self.mtime = mtime self.mtime = mtime
self.good_quality = False self.good_quality = False
logger.info('sound file {} m_time has changed. Reset quality info' logger.info('sound %s: m_time has changed. Reset quality info',
.format(self.path)) self.path)
return True return True
return old_removed != self.removed return old_removed != self.removed
@ -255,7 +255,7 @@ class Schedule (models.Model):
'last': (0b010000, _('last week of the month')), 'last': (0b010000, _('last week of the month')),
'first and third': (0b000101, _('first and third weeks of the month')), 'first and third': (0b000101, _('first and third weeks of the month')),
'second and fourth': (0b001010, _('second and fourth weeks of the month')), 'second and fourth': (0b001010, _('second and fourth weeks of the month')),
'every': (0b011111, _('once a week')), 'every': (0b011111, _('every week')),
'one on two': (0b100000, _('one week on two')), 'one on two': (0b100000, _('one week on two')),
} }
VerboseFrequency = { value[0]: value[1] for key, value in Frequency.items() } VerboseFrequency = { value[0]: value[1] for key, value in Frequency.items() }
@ -323,40 +323,42 @@ class Schedule (models.Model):
Return a list with all matching dates of date.month (=today) Return a list with all matching dates of date.month (=today)
""" """
date = date_or_default(date, True).replace(day=1) date = date_or_default(date, True).replace(day=1)
fwday = date.weekday() freq = self.frequency
wday = self.date.weekday()
# move date to the date weekday of the schedule # move to the first day of the month that matches the schedule's weekday
# check on SO#3284452 for the formula # check on SO#3284452 for the formula
date += tz.timedelta(days = (7 if fwday > wday else 0) - fwday + wday) first_weekday = date.weekday()
fwday = date.weekday() sched_weekday = self.date.weekday()
date += tz.timedelta(days = (7 if first_weekday > sched_weekday else 0) \
- first_weekday + sched_weekday)
month = date.month
# special frequency case # last of the month
weeks = self.frequency if freq == Schedule.Frequency['last']:
if self.frequency == Schedule.Frequency['last']: date += tz.timedelta(days = 4 * 7)
date += tz.timedelta(month = 1, days = -7) next_date = date + tz.timedelta(days = 7)
return self.normalize([date]) if next_date.month == month:
if weeks == Schedule.Frequency['one on two']: date = next_date
# if both week are the same, then the date week of the month return [self.normalize(date)]
# matches. Note: wday % 2 + fwday % 2 => (wday + fwday) % 2
fweek = date.isocalendar()[1]
if date.month == 1 and fweek >= 50:
# isocalendar can think we are on the last week of the
# previous year
fweek = 0
week = self.date.isocalendar()[1]
weeks = 0b010101 if not (fweek + week) % 2 else 0b001010
dates = [] dates = []
for week in range(0,5): if freq == Schedule.Frequency['one on two']:
# there can be five weeks in a month # NOTE previous algorithm was based on the week number, but this
if not weeks & (0b1 << week): # approach is wrong because number of weeks in a year can be
continue # 52 or 53. This also clashes with the first week of the year.
wdate = date + tz.timedelta(days = week * 7) if not (date - self.date).days % 14:
if wdate.month == date.month: date += tz.timedelta(days = 7)
dates.append(self.normalize(wdate))
return dates while date.month == month:
dates.append(date)
date += tz.timedelta(days = 14)
else:
week = 0
while week < 5 and date.month == month:
if freq & (0b1 << week):
dates.append(date)
date += tz.timedelta(days = 7)
return [self.normalize(date) for date in dates]
def diffusions_of_month (self, date, exclude_saved = False): def diffusions_of_month (self, date, exclude_saved = False):
""" """
@ -397,9 +399,16 @@ class Schedule (models.Model):
return diffusions return diffusions
def __str__ (self): def __str__ (self):
frequency = [ x for x,y in Schedule.Frequency.items() return ' | '.join([ '#' + str(self.id), self.program.name,
if y == self.frequency ] self.get_frequency_display(),
return self.program.name + ': ' + frequency[0] + ' (' + str(self.date) + ')' self.date.strftime('%a %H:%M') ])
def save (self, *args, **kwargs):
if self.initial:
self.program = self.initial.program
self.duration = self.initial.duration
self.frequency = self.initial.frequency
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Schedule') verbose_name = _('Schedule')
@ -489,16 +498,24 @@ class Program (Nameable):
def __init__ (self, *kargs, **kwargs): def __init__ (self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
self.__original_path = self.path if self.name:
self.__original_path = self.path
def save (self, *kargs, **kwargs): def save (self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().save(*kargs, **kwargs)
if self.__original_path != self.path and \ if hasattr(self, '__original_path') and \
self.__original_path != self.path and \
os.path.exists(self.__original_path) and \
not os.path.exists(self.path): not os.path.exists(self.path):
logger.info('program {} name changed to {}. Change dir name' \ logger.info('program #%s\'s name changed to %s. Change dir name',
.format(self.id, self.name)) self.id, self.name)
shutil.move(self.__original_path, self.path) shutil.move(self.__original_path, self.path)
sounds = Sounds.objects.filter(path__startswith = self.__original_path)
for sound in sounds:
sound.path.replace(self.__original_path, self.path)
sound.save()
class Diffusion (models.Model): class Diffusion (models.Model):
""" """
A Diffusion is an occurrence of a Program that is scheduled on the A Diffusion is an occurrence of a Program that is scheduled on the
@ -624,7 +641,7 @@ class Diffusion (models.Model):
if diff.pk == self.pk: if diff.pk == self.pk:
continue continue
if diff.date < end: if diff.date < end and diff not in r:
r.append(diff) r.append(diff)
continue continue
count+=1 count+=1
@ -637,10 +654,10 @@ class Diffusion (models.Model):
if self.initial.initial: if self.initial.initial:
self.initial = self.initial.initial self.initial = self.initial.initial
self.program = self.initial.program self.program = self.initial.program
super(Diffusion, self).save(*args, **kwargs) super().save(*args, **kwargs)
def __str__ (self): def __str__ (self):
return self.program.name + ', ' + \ return '#' + str(self.pk) + ' ' + self.program.name + ', ' + \
self.date.strftime('%Y-%m-%d %H:%M') +\ self.date.strftime('%Y-%m-%d %H:%M') +\
'' # FIXME str(self.type_display) '' # FIXME str(self.type_display)
@ -691,15 +708,15 @@ class Log (models.Model):
ContentType.objects.get_for_model(model).id) ContentType.objects.get_for_model(model).id)
def print (self): def print (self):
logger.info('log #{} ({}): {}{}'.format( logger.info('log #%s: %s%s',
str(self), str(self),
self.comment or '', self.comment or '',
'\n - {}: #{}'.format(self.related_type, self.related_id) '\n - {}: #{}'.format(self.related_type, self.related_id)
if self.related_object else '' if self.related_object else ''
)) )
def __str__ (self): def __str__ (self):
return 'log #{} ({}, {})'.format( return '#{} ({}, {})'.format(
self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source
) )