diff --git a/liquidsoap/templates/aircox/liquidsoap/controller.html b/liquidsoap/templates/aircox/liquidsoap/controller.html
index 4a46241..518305e 100644
--- a/liquidsoap/templates/aircox/liquidsoap/controller.html
+++ b/liquidsoap/templates/aircox/liquidsoap/controller.html
@@ -106,15 +106,15 @@
{% with source=controller.master %}
- {% include 'aircox_liquidsoap/source.html' %}
+ {% include 'aircox/liquidsoap/source.html' %}
{% endwith %}
{% with source=controller.dealer %}
- {% include 'aircox_liquidsoap/source.html' %}
+ {% include 'aircox/liquidsoap/source.html' %}
{% endwith %}
{% for source in controller.streams.values %}
- {% include 'aircox_liquidsoap/source.html' %}
+ {% include 'aircox/liquidsoap/source.html' %}
{% endfor %}
diff --git a/liquidsoap/templates/aircox/liquidsoap/station.liq b/liquidsoap/templates/aircox/liquidsoap/station.liq
index 3456456..3f52214 100644
--- a/liquidsoap/templates/aircox/liquidsoap/station.liq
+++ b/liquidsoap/templates/aircox/liquidsoap/station.liq
@@ -4,7 +4,8 @@
def interactive_source (id, s) = \
def handler(m) = \
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 \
\
s = on_track(id=id, handler, s)
@@ -65,7 +66,7 @@ set("{{ key|safe }}", {{ value|safe }}) \
{% if controller.station.fallback %}
single("{{ controller.station.fallback }}"), \
{% else %}
- blank(), \
+ blank(duration=0.1), \
{% endif %}
]) \
) \
diff --git a/liquidsoap/utils.py b/liquidsoap/utils.py
index e97710f..00b5ebf 100644
--- a/liquidsoap/utils.py
+++ b/liquidsoap/utils.py
@@ -173,7 +173,7 @@ class Source:
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:
return
@@ -279,6 +279,38 @@ class Dealer (Source):
if diffusion.playlist and on_air not in diffusion.playlist:
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:
"""
@@ -385,7 +417,6 @@ class Controller:
"""
if not self.connector.available and self.connector.open():
return
-
self.dealer.monitor()
diff --git a/programs/admin.py b/programs/admin.py
index 5b374c3..f47d3fc 100755
--- a/programs/admin.py
+++ b/programs/admin.py
@@ -3,6 +3,7 @@ import copy
from django import forms
from django.contrib import admin
from django.db import models
+from django.utils.translation import ugettext as _, ugettext_lazy
from aircox.programs.models import *
@@ -64,6 +65,12 @@ class StationAdmin (NameableAdmin):
@admin.register(Program)
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' ]
# TODO list_display
inlines = [ ScheduleInline, StreamInline ]
@@ -92,7 +99,7 @@ class DiffusionAdmin (admin.ModelAdmin):
list_filter = ('type', 'date', 'program')
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):
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_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)
diff --git a/programs/management/commands/diffusions_monitor.py b/programs/management/commands/diffusions_monitor.py
index 70d60ab..39da99c 100644
--- a/programs/management/commands/diffusions_monitor.py
+++ b/programs/management/commands/diffusions_monitor.py
@@ -23,7 +23,7 @@ from django.utils import timezone as tz
from aircox.programs.models import *
-logger = logging.getLogger('aircox.programs.' + __name__)
+logger = logging.getLogger('aircox.tools')
class Actions:
@staticmethod
@@ -33,19 +33,28 @@ class Actions:
items if they have been generated during this
update.
+ It set an attribute 'do_not_save' if the item should not
+ be saved. FIXME: find proper way
+
Return the number of 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:
item.type = Diffusion.Type['normal']
return 0
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)
@classmethod
@@ -67,14 +76,18 @@ class Actions:
else:
for item in items:
count[1] += cl.__check_conflicts(item, saved_items)
+ if hasattr(item, 'do_not_save'):
+ count[0] -= 1
+ continue
+
item.save()
saved_items.add(item)
- logger.info('[update] {} new diffusions for schedule #{} ({})'.format(
- len(items), schedule.id, str(schedule)
- ))
+ logger.info('[update] schedule %s: %d new diffusions',
+ 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
'{} conflicts found'.format(count[1]))
@@ -82,7 +95,7 @@ class Actions:
def clean (date):
qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'],
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()
@staticmethod
@@ -98,7 +111,7 @@ class Actions:
else:
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):
Diffusion.objects.filter(id__in = items).delete()
diff --git a/programs/management/commands/sounds_monitor.py b/programs/management/commands/sounds_monitor.py
index 2074e4d..e239f24 100644
--- a/programs/management/commands/sounds_monitor.py
+++ b/programs/management/commands/sounds_monitor.py
@@ -33,16 +33,17 @@ from aircox.programs.models import *
import aircox.programs.settings as settings
import aircox.programs.utils as utils
-logger = logging.getLogger('aircox.programs.' + __name__)
+logger = logging.getLogger('aircox.tools')
class Command (BaseCommand):
help= __doc__
def report (self, program = None, component = None, *content):
if not component:
- logger.info('{}: '.format(program), *content)
+ logger.info('%s: %s', str(program), ' '.join([str(c) for c in content]))
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):
parser.formatter_class=RawTextHelpFormatter
@@ -70,7 +71,7 @@ class Command (BaseCommand):
if not err:
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
format (given in Command.help)
@@ -92,7 +93,6 @@ class Command (BaseCommand):
else:
r = r.groupdict()
- r['duration'] = self._get_duration(path)
r['name'] = r['name'].replace('_', ' ').capitalize()
r['path'] = path
return r
@@ -132,11 +132,11 @@ class Command (BaseCommand):
"""
For all programs, scan dirs
"""
- logger.info('scan files for all programs...')
+ logger.info('scan all programs...')
programs = Program.objects.filter()
for program in programs:
- logger.info('program', program.name)
+ logger.info('#%d %s', program.id, program.name)
self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'],
@@ -151,7 +151,7 @@ class Command (BaseCommand):
Scan a given directory that is associated to the given program, and
update sounds information.
"""
- logger.info('scan files in', subdir)
+ logger.info('- %s/', subdir)
if not program.ensure_dir(subdir):
return
@@ -163,14 +163,17 @@ class Command (BaseCommand):
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
continue
- sound_info = self.get_sound_info(program, path)
- sound = Sound.objects.get_or_create(
+ sound, created = Sound.objects.get_or_create(
path = path,
- defaults = { 'name': sound_info['name'],
- 'duration': sound_info['duration'] or None }
- )[0]
- sound.__dict__.update(sound_kwargs)
- sound.save(check = False)
+ defaults = sound_kwargs,
+ )
+
+ sound_info = self._get_sound_info(program, path)
+
+ 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
if 'year' in sound_info:
@@ -181,12 +184,12 @@ class Command (BaseCommand):
self.report(program, path,
'the diffusion must be an initial diffusion')
else:
- sound = initial.sounds.get_queryset() \
- .filter(path == sound.path)
- if not sound:
+ sound_ = initial.sounds.get_queryset() \
+ .filter(path = sound.path)
+ if not sound_:
self.report(program, path,
'add sound to diffusion ', initial.id)
- initial.sounds.add(sound)
+ initial.sounds.add(sound.pk)
initial.save()
self.check_sounds(Sound.objects.filter(path__startswith = subdir))
@@ -201,16 +204,18 @@ class Command (BaseCommand):
sounds = Sound.objects.filter(good_quality = False)
if check:
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:
- 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.handle( files = files,
**settings.AIRCOX_SOUND_QUALITY )
- logger.info('update sounds in database')
+ logger.info('update database')
def update_stats(sound_info, sound):
stats = sound_info.get_file_stats()
if stats:
diff --git a/programs/management/commands/sounds_quality_check.py b/programs/management/commands/sounds_quality_check.py
index 2922e67..846a51c 100644
--- a/programs/management/commands/sounds_quality_check.py
+++ b/programs/management/commands/sounds_quality_check.py
@@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
-logger = logging.getLogger('aircox.programs.' + __name__)
+logger = logging.getLogger('aircox.tools')
class Stats:
attributes = [
@@ -104,11 +104,11 @@ class Sound:
]
if self.good:
- logger.info(self.path, ': good samples:\033[92m',
- ', '.join( view(self.good) ), '\033[0m')
+ logger.info(self.path + ': good samples:\033[92m%s\033[0m',
+ ', '.join(view(self.good)))
if self.bad:
- loggeer.info(self.path ': bad samples:\033[91m',
- ', '.join( view(self.bad) ), '\033[0m')
+ logger.info(self.path + ': good samples:\033[91m%s\033[0m',
+ ', '.join(view(self.bad)))
class Command (BaseCommand):
help = __doc__
@@ -157,7 +157,7 @@ class Command (BaseCommand):
self.bad = []
self.good = []
for sound in self.sounds:
- logger.info('analyse ', sound.path)
+ logger.info('analyse ' + sound.path)
sound.analyse()
sound.check(attr, minmax[0], minmax[1])
if sound.bad:
@@ -167,13 +167,8 @@ class Command (BaseCommand):
# resume
if options.get('resume'):
- if self.good:
- logger.info('files that did not failed the test:\033[92m\n ',
- '\n '.join([sound.path for sound in self.good]),
- '\033[0m')
- 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')
+ for sound in self.good:
+ logger.info('\033[92m+ %s\033[0m', sound.path)
+ for sound in self.bad:
+ logger.info('\033[91m+ %s\033[0m', sound.path)
diff --git a/programs/models.py b/programs/models.py
index c10308d..9a8859c 100755
--- a/programs/models.py
+++ b/programs/models.py
@@ -16,7 +16,7 @@ import aircox.programs.utils as utils
import aircox.programs.settings as settings
-logger = logging.getLogger(__name__)
+logger = logging.getLogger('aircox.core')
def date_or_default (date, date_only = False):
@@ -171,7 +171,7 @@ class Sound (Nameable):
if not self.file_exists():
if self.removed:
return
- logger.info('sound file {} has been removed'.format(self.path))
+ logger.info('sound %s: has been removed', self.path)
self.removed = True
return True
@@ -182,8 +182,8 @@ class Sound (Nameable):
if self.mtime != mtime:
self.mtime = mtime
self.good_quality = False
- logger.info('sound file {} m_time has changed. Reset quality info'
- .format(self.path))
+ logger.info('sound %s: m_time has changed. Reset quality info',
+ self.path)
return True
return old_removed != self.removed
@@ -255,7 +255,7 @@ class Schedule (models.Model):
'last': (0b010000, _('last week 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')),
- 'every': (0b011111, _('once a week')),
+ 'every': (0b011111, _('every week')),
'one on two': (0b100000, _('one week on two')),
}
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)
"""
date = date_or_default(date, True).replace(day=1)
- fwday = date.weekday()
- wday = self.date.weekday()
+ freq = self.frequency
- # 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
- date += tz.timedelta(days = (7 if fwday > wday else 0) - fwday + wday)
- fwday = date.weekday()
+ first_weekday = 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
- weeks = self.frequency
- if self.frequency == Schedule.Frequency['last']:
- date += tz.timedelta(month = 1, days = -7)
- return self.normalize([date])
- if weeks == Schedule.Frequency['one on two']:
- # if both week are the same, then the date week of the month
- # 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
+ # last of the month
+ if freq == Schedule.Frequency['last']:
+ date += tz.timedelta(days = 4 * 7)
+ next_date = date + tz.timedelta(days = 7)
+ if next_date.month == month:
+ date = next_date
+ return [self.normalize(date)]
dates = []
- for week in range(0,5):
- # there can be five weeks in a month
- if not weeks & (0b1 << week):
- continue
- wdate = date + tz.timedelta(days = week * 7)
- if wdate.month == date.month:
- dates.append(self.normalize(wdate))
- return dates
+ if freq == Schedule.Frequency['one on two']:
+ # NOTE previous algorithm was based on the week number, but this
+ # approach is wrong because number of weeks in a year can be
+ # 52 or 53. This also clashes with the first week of the year.
+ if not (date - self.date).days % 14:
+ date += tz.timedelta(days = 7)
+
+ 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):
"""
@@ -397,9 +399,16 @@ class Schedule (models.Model):
return diffusions
def __str__ (self):
- frequency = [ x for x,y in Schedule.Frequency.items()
- if y == self.frequency ]
- return self.program.name + ': ' + frequency[0] + ' (' + str(self.date) + ')'
+ return ' | '.join([ '#' + str(self.id), self.program.name,
+ self.get_frequency_display(),
+ 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:
verbose_name = _('Schedule')
@@ -489,16 +498,24 @@ class Program (Nameable):
def __init__ (self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs)
- self.__original_path = self.path
+ if self.name:
+ self.__original_path = self.path
def save (self, *kargs, **kwargs):
- super().__init__(*kargs, **kwargs)
- if self.__original_path != self.path and \
+ super().save(*kargs, **kwargs)
+ 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):
- logger.info('program {} name changed to {}. Change dir name' \
- .format(self.id, self.name))
+ logger.info('program #%s\'s name changed to %s. Change dir name',
+ self.id, self.name)
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):
"""
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:
continue
- if diff.date < end:
+ if diff.date < end and diff not in r:
r.append(diff)
continue
count+=1
@@ -637,10 +654,10 @@ class Diffusion (models.Model):
if self.initial.initial:
self.initial = self.initial.initial
self.program = self.initial.program
- super(Diffusion, self).save(*args, **kwargs)
+ super().save(*args, **kwargs)
def __str__ (self):
- return self.program.name + ', ' + \
+ return '#' + str(self.pk) + ' ' + self.program.name + ', ' + \
self.date.strftime('%Y-%m-%d %H:%M') +\
'' # FIXME str(self.type_display)
@@ -691,15 +708,15 @@ class Log (models.Model):
ContentType.objects.get_for_model(model).id)
def print (self):
- logger.info('log #{} ({}): {}{}'.format(
+ logger.info('log #%s: %s%s',
str(self),
self.comment or '',
'\n - {}: #{}'.format(self.related_type, self.related_id)
if self.related_object else ''
- ))
+ )
def __str__ (self):
- return 'log #{} ({}, {})'.format(
+ return '#{} ({}, {})'.format(
self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source
)