forked from rc/aircox
work on website; fix stuffs on aircox too
This commit is contained in:
@ -84,8 +84,8 @@ class StationAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Log)
|
||||
class LogAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
|
||||
list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
|
||||
list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track']
|
||||
list_filter = ['date', 'source', 'station']
|
||||
|
||||
admin.site.register(Port)
|
||||
|
||||
|
@ -34,7 +34,7 @@ class DiffusionAdmin(admin.ModelAdmin):
|
||||
conflicts_count.short_description = _('Conflicts')
|
||||
|
||||
def start_date(self, obj):
|
||||
return obj.local_date.strftime('%Y/%m/%d %H:%M')
|
||||
return obj.local_start.strftime('%Y/%m/%d %H:%M')
|
||||
start_date.short_description = _('start')
|
||||
|
||||
def end_date(self, obj):
|
||||
|
@ -18,11 +18,10 @@ class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
|
||||
@admin.register(Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
# TODO: url to filter by tag
|
||||
def tag_list(self, obj):
|
||||
return u", ".join(o.name for o in obj.tags.all())
|
||||
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound']
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound', 'timestamp']
|
||||
list_editable = ['artist', 'title']
|
||||
list_filter = ['sound', 'diffusion', 'artist', 'title', 'tags']
|
||||
fieldsets = [
|
||||
|
@ -16,14 +16,14 @@ logger = logging.getLogger('aircox.tools')
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
help= __doc__
|
||||
help = __doc__
|
||||
|
||||
def add_arguments (self, parser):
|
||||
parser.formatter_class=RawTextHelpFormatter
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
group = parser.add_argument_group('actions')
|
||||
group.add_argument(
|
||||
'-a', '--age', type=int,
|
||||
default = settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE,
|
||||
default=settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE,
|
||||
help='minimal age in days of logs to archive. Default is '
|
||||
'settings.AIRCOX_LOGS_ARCHIVES_MIN_AGE'
|
||||
)
|
||||
@ -36,22 +36,21 @@ class Command (BaseCommand):
|
||||
help='keep logs in database instead of deleting them'
|
||||
)
|
||||
|
||||
def handle (self, *args, age, force, keep, **options):
|
||||
date = tz.now() - tz.timedelta(days = age)
|
||||
def handle(self, *args, age, force, keep, **options):
|
||||
date = tz.now() - tz.timedelta(days=age)
|
||||
|
||||
while True:
|
||||
date = date.replace(
|
||||
hour = 0, minute = 0, second = 0, microsecond = 0
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
|
||||
logger.info('archive log at date %s', date)
|
||||
for station in Station.objects.all():
|
||||
Log.objects.make_archive(
|
||||
station, date, force = force, keep = keep
|
||||
station, date, force=force, keep=keep
|
||||
)
|
||||
|
||||
qs = Log.objects.filter(date__lt = date)
|
||||
qs = Log.objects.filter(date__lt=date)
|
||||
if not qs.exists():
|
||||
break
|
||||
date = qs.order_by('-date').first().date
|
||||
|
||||
|
@ -15,6 +15,7 @@ planified before the (given) month.
|
||||
- "check" will remove all diffusions that are unconfirmed and have been planified
|
||||
from the (given) month and later.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from argparse import RawTextHelpFormatter
|
||||
|
||||
@ -25,53 +26,52 @@ from aircox.models import *
|
||||
|
||||
logger = logging.getLogger('aircox.tools')
|
||||
|
||||
import time
|
||||
|
||||
class Actions:
|
||||
@classmethod
|
||||
def update (cl, date, mode):
|
||||
def update(cl, date, mode):
|
||||
manual = (mode == 'manual')
|
||||
|
||||
count = [0, 0]
|
||||
for schedule in Schedule.objects.filter(program__active = True) \
|
||||
for schedule in Schedule.objects.filter(program__active=True) \
|
||||
.order_by('initial'):
|
||||
# in order to allow rerun links between diffusions, we save items
|
||||
# by schedule;
|
||||
items = schedule.diffusions_of_month(date, exclude_saved = True)
|
||||
items = schedule.diffusions_of_month(date, exclude_saved=True)
|
||||
count[0] += len(items)
|
||||
|
||||
# we can't bulk create because we need signal processing
|
||||
for item in items:
|
||||
conflicts = item.get_conflicts()
|
||||
item.type = Diffusion.Type.unconfirmed \
|
||||
if manual or conflicts.count() else \
|
||||
Diffusion.Type.normal
|
||||
item.save(no_check = True)
|
||||
if manual or conflicts.count() else \
|
||||
Diffusion.Type.normal
|
||||
item.save(no_check=True)
|
||||
if conflicts.count():
|
||||
item.conflicts.set(conflicts.all())
|
||||
|
||||
logger.info('[update] schedule %s: %d new diffusions',
|
||||
str(schedule), len(items),
|
||||
)
|
||||
str(schedule), len(items),
|
||||
)
|
||||
|
||||
logger.info('[update] %d diffusions have been created, %s', count[0],
|
||||
'do not forget manual approval' if manual else
|
||||
'{} conflicts found'.format(count[1]))
|
||||
'do not forget manual approval' if manual else
|
||||
'{} conflicts found'.format(count[1]))
|
||||
|
||||
@staticmethod
|
||||
def clean (date):
|
||||
qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
|
||||
start__lt = date)
|
||||
def clean(date):
|
||||
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
|
||||
start__lt=date)
|
||||
logger.info('[clean] %d diffusions will be removed', qs.count())
|
||||
qs.delete()
|
||||
|
||||
@staticmethod
|
||||
def check(date):
|
||||
qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
|
||||
start__gt = date)
|
||||
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
|
||||
start__gt=date)
|
||||
items = []
|
||||
for diffusion in qs:
|
||||
schedules = Schedule.objects.filter(program = diffusion.program)
|
||||
schedules = Schedule.objects.filter(program=diffusion.program)
|
||||
for schedule in schedules:
|
||||
if schedule.match(diffusion.start):
|
||||
break
|
||||
@ -80,14 +80,14 @@ class Actions:
|
||||
|
||||
logger.info('[check] %d diffusions will be removed', len(items))
|
||||
if len(items):
|
||||
Diffusion.objects.filter(id__in = items).delete()
|
||||
Diffusion.objects.filter(id__in=items).delete()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help= __doc__
|
||||
help = __doc__
|
||||
|
||||
def add_arguments (self, parser):
|
||||
parser.formatter_class=RawTextHelpFormatter
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
now = tz.datetime.today()
|
||||
|
||||
group = parser.add_argument_group('action')
|
||||
@ -130,23 +130,22 @@ class Command(BaseCommand):
|
||||
'diffusions except those that conflicts with others'
|
||||
)
|
||||
|
||||
def handle (self, *args, **options):
|
||||
date = tz.datetime(year = options.get('year'),
|
||||
month = options.get('month'),
|
||||
day = 1)
|
||||
def handle(self, *args, **options):
|
||||
date = tz.datetime(year=options.get('year'),
|
||||
month=options.get('month'),
|
||||
day=1)
|
||||
date = tz.make_aware(date)
|
||||
if options.get('next_month'):
|
||||
month = options.get('month')
|
||||
date += tz.timedelta(days = 28)
|
||||
date += tz.timedelta(days=28)
|
||||
if date.month == month:
|
||||
date += tz.timedelta(days = 28)
|
||||
date += tz.timedelta(days=28)
|
||||
|
||||
date = date.replace(day = 1)
|
||||
date = date.replace(day=1)
|
||||
|
||||
if options.get('update'):
|
||||
Actions.update(date, mode = options.get('mode'))
|
||||
Actions.update(date, mode=options.get('mode'))
|
||||
if options.get('clean'):
|
||||
Actions.clean(date)
|
||||
if options.get('check'):
|
||||
Actions.check(date)
|
||||
|
||||
|
@ -20,7 +20,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from aircox.models import *
|
||||
import aircox.settings as settings
|
||||
__doc__ = __doc__.format(settings = settings)
|
||||
__doc__ = __doc__.format(settings=settings)
|
||||
|
||||
logger = logging.getLogger('aircox.tools')
|
||||
|
||||
@ -78,8 +78,8 @@ class Importer:
|
||||
return
|
||||
try:
|
||||
timestamp = int(line.get('minutes') or 0) * 60 + \
|
||||
int(line.get('seconds') or 0) \
|
||||
if has_timestamp else None
|
||||
int(line.get('seconds') or 0) \
|
||||
if has_timestamp else None
|
||||
|
||||
track, created = Track.objects.get_or_create(
|
||||
title=line.get('title'),
|
||||
@ -88,6 +88,7 @@ class Importer:
|
||||
**self.track_kwargs
|
||||
)
|
||||
track.timestamp = timestamp
|
||||
print('track', track, timestamp)
|
||||
track.info = line.get('info')
|
||||
tags = line.get('tags')
|
||||
if tags:
|
||||
@ -96,7 +97,7 @@ class Importer:
|
||||
logger.warning(
|
||||
'an error occured for track {index}, it may not '
|
||||
'have been saved: {err}'
|
||||
.format(index = index, err=err)
|
||||
.format(index=index, err=err)
|
||||
)
|
||||
continue
|
||||
|
||||
@ -107,10 +108,10 @@ class Importer:
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
help= __doc__
|
||||
help = __doc__
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class=RawTextHelpFormatter
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
parser.add_argument(
|
||||
'path', metavar='PATH', type=str,
|
||||
help='path of the input playlist to read'
|
||||
@ -125,7 +126,7 @@ class Command (BaseCommand):
|
||||
help='try to get the diffusion relative to the sound if it exists'
|
||||
)
|
||||
|
||||
def handle (self, path, *args, **options):
|
||||
def handle(self, path, *args, **options):
|
||||
# FIXME: absolute/relative path of sounds vs given path
|
||||
if options.get('sound'):
|
||||
sound = Sound.objects.filter(
|
||||
@ -136,7 +137,7 @@ class Command (BaseCommand):
|
||||
sound = Sound.objects.filter(path__icontains=path_).first()
|
||||
|
||||
if not sound:
|
||||
logger.error('no sound found in the database for the path ' \
|
||||
logger.error('no sound found in the database for the path '
|
||||
'{path}'.format(path=path))
|
||||
return
|
||||
|
||||
@ -148,4 +149,3 @@ class Command (BaseCommand):
|
||||
logger.info('track #{pos} imported: {title}, by {artist}'.format(
|
||||
pos=track.position, title=track.title, artist=track.artist
|
||||
))
|
||||
|
||||
|
@ -11,6 +11,7 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
logger = logging.getLogger('aircox.tools')
|
||||
|
||||
|
||||
class Stats:
|
||||
attributes = [
|
||||
'DC offset', 'Min level', 'Max level',
|
||||
@ -18,7 +19,7 @@ class Stats:
|
||||
'RMS Tr dB', 'Flat factor', 'Length s',
|
||||
]
|
||||
|
||||
def __init__ (self, path, **kwargs):
|
||||
def __init__(self, path, **kwargs):
|
||||
"""
|
||||
If path is given, call analyse with path and kwargs
|
||||
"""
|
||||
@ -26,10 +27,10 @@ class Stats:
|
||||
if path:
|
||||
self.analyse(path, **kwargs)
|
||||
|
||||
def get (self, attr):
|
||||
def get(self, attr):
|
||||
return self.values.get(attr)
|
||||
|
||||
def parse (self, output):
|
||||
def parse(self, output):
|
||||
for attr in Stats.attributes:
|
||||
value = re.search(attr + r'\s+(?P<value>\S+)', output)
|
||||
value = value and value.groupdict()
|
||||
@ -41,14 +42,14 @@ class Stats:
|
||||
self.values[attr] = value
|
||||
self.values['length'] = self.values['Length s']
|
||||
|
||||
def analyse (self, path, at = None, length = None):
|
||||
def analyse(self, path, at=None, length=None):
|
||||
"""
|
||||
If at and length are given use them as excerpt to analyse.
|
||||
"""
|
||||
args = ['sox', path, '-n']
|
||||
|
||||
if at is not None and length is not None:
|
||||
args += ['trim', str(at), str(length) ]
|
||||
args += ['trim', str(at), str(length)]
|
||||
|
||||
args.append('stats')
|
||||
|
||||
@ -66,17 +67,17 @@ class Sound:
|
||||
bad = None # list of bad samples
|
||||
good = None # list of good samples
|
||||
|
||||
def __init__ (self, path, sample_length = None):
|
||||
def __init__(self, path, sample_length=None):
|
||||
self.path = path
|
||||
self.sample_length = sample_length if sample_length is not None \
|
||||
else self.sample_length
|
||||
else self.sample_length
|
||||
|
||||
def get_file_stats (self):
|
||||
def get_file_stats(self):
|
||||
return self.stats and self.stats[0]
|
||||
|
||||
def analyse (self):
|
||||
def analyse(self):
|
||||
logger.info('complete file analysis')
|
||||
self.stats = [ Stats(self.path) ]
|
||||
self.stats = [Stats(self.path)]
|
||||
position = 0
|
||||
length = self.stats[0].get('length')
|
||||
|
||||
@ -85,21 +86,22 @@ class Sound:
|
||||
|
||||
logger.info('start samples analysis...')
|
||||
while position < length:
|
||||
stats = Stats(self.path, at = position, length = self.sample_length)
|
||||
stats = Stats(self.path, at=position, length=self.sample_length)
|
||||
self.stats.append(stats)
|
||||
position += self.sample_length
|
||||
|
||||
def check (self, name, min_val, max_val):
|
||||
self.good = [ index for index, stats in enumerate(self.stats)
|
||||
if min_val <= stats.get(name) <= max_val ]
|
||||
self.bad = [ index for index, stats in enumerate(self.stats)
|
||||
if index not in self.good ]
|
||||
def check(self, name, min_val, max_val):
|
||||
self.good = [index for index, stats in enumerate(self.stats)
|
||||
if min_val <= stats.get(name) <= max_val]
|
||||
self.bad = [index for index, stats in enumerate(self.stats)
|
||||
if index not in self.good]
|
||||
self.resume()
|
||||
|
||||
def resume (self):
|
||||
view = lambda array: [
|
||||
def resume(self):
|
||||
def view(array): return [
|
||||
'file' if index is 0 else
|
||||
'sample {} (at {} seconds)'.format(index, (index-1) * self.sample_length)
|
||||
'sample {} (at {} seconds)'.format(
|
||||
index, (index-1) * self.sample_length)
|
||||
for index in array
|
||||
]
|
||||
|
||||
@ -110,12 +112,13 @@ class Sound:
|
||||
logger.info(self.path + ' -> bad: \033[91m%s\033[0m',
|
||||
', '.join(view(self.bad)))
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
help = __doc__
|
||||
sounds = None
|
||||
|
||||
def add_arguments (self, parser):
|
||||
parser.formatter_class=RawTextHelpFormatter
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
|
||||
parser.add_argument(
|
||||
'files', metavar='FILE', type=str, nargs='+',
|
||||
@ -128,12 +131,12 @@ class Command (BaseCommand):
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--attribute', type=str,
|
||||
help='attribute name to use to check, that can be:\n' + \
|
||||
', '.join([ '"{}"'.format(attr) for attr in Stats.attributes ])
|
||||
help='attribute name to use to check, that can be:\n' +
|
||||
', '.join(['"{}"'.format(attr) for attr in Stats.attributes])
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--range', type=float, nargs=2,
|
||||
help='range of minimal and maximal accepted value such as: ' \
|
||||
help='range of minimal and maximal accepted value such as: '
|
||||
'--range min max'
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -141,7 +144,7 @@ class Command (BaseCommand):
|
||||
help='print a resume of good and bad files'
|
||||
)
|
||||
|
||||
def handle (self, *args, **options):
|
||||
def handle(self, *args, **options):
|
||||
# parameters
|
||||
minmax = options.get('range')
|
||||
if not minmax:
|
||||
@ -152,8 +155,8 @@ class Command (BaseCommand):
|
||||
raise CommandError('no attribute specified')
|
||||
|
||||
# sound analyse and checks
|
||||
self.sounds = [ Sound(path, options.get('sample_length'))
|
||||
for path in options.get('files') ]
|
||||
self.sounds = [Sound(path, options.get('sample_length'))
|
||||
for path in options.get('files')]
|
||||
self.bad = []
|
||||
self.good = []
|
||||
for sound in self.sounds:
|
||||
@ -171,4 +174,3 @@ class Command (BaseCommand):
|
||||
logger.info('\033[92m+ %s\033[0m', sound.path)
|
||||
for sound in self.bad:
|
||||
logger.info('\033[91m+ %s\033[0m', sound.path)
|
||||
|
||||
|
@ -6,6 +6,7 @@ used to:
|
||||
- cancels Diffusions that have an archive but could not have been played;
|
||||
- run Liquidsoap
|
||||
"""
|
||||
import tzlocal
|
||||
import time
|
||||
import re
|
||||
|
||||
@ -28,7 +29,6 @@ tz.activate(pytz.UTC)
|
||||
# FIXME liquidsoap does not manage timezones -- we have to convert
|
||||
# 'on_air' metadata we get from it into utc one in order to work
|
||||
# correctly.
|
||||
import tzlocal
|
||||
local_tz = tzlocal.get_localzone()
|
||||
|
||||
|
||||
@ -118,10 +118,12 @@ class Monitor:
|
||||
self.handle()
|
||||
|
||||
def log(self, date=None, **kwargs):
|
||||
"""
|
||||
Create a log using **kwargs, and print info
|
||||
"""
|
||||
""" Create a log using **kwargs, and print info """
|
||||
log = Log(station=self.station, date=date or tz.now(), **kwargs)
|
||||
if log.type == Log.Type.on_air and log.diffusion is None:
|
||||
log.collision = Diffusion.objects.station(log.station) \
|
||||
.on_air().at(log.date).first()
|
||||
|
||||
log.save()
|
||||
log.print()
|
||||
return log
|
||||
@ -153,7 +155,7 @@ class Monitor:
|
||||
# check for reruns
|
||||
if not diff.is_date_in_range(air_time) and not diff.initial:
|
||||
diff = Diffusion.objects.at(air_time) \
|
||||
.filter(initial=diff).first()
|
||||
.on_air().filter(initial=diff).first()
|
||||
|
||||
# log sound on air
|
||||
return self.log(
|
||||
@ -170,14 +172,14 @@ class Monitor:
|
||||
if log.diffusion:
|
||||
return
|
||||
|
||||
tracks = Track.objects.filter(sound=log.sound, timestamp_isnull=False)
|
||||
tracks = Track.objects.filter(sound=log.sound, timestamp__isnull=False)
|
||||
if not tracks.exists():
|
||||
return
|
||||
|
||||
tracks = tracks.exclude(log__station=self.station, log__pk__gt=log.pk)
|
||||
now = tz.now()
|
||||
for track in tracks:
|
||||
pos = log.date + tz.timedelta(seconds=track.position)
|
||||
pos = log.date + tz.timedelta(seconds=track.timestamp)
|
||||
if pos > now:
|
||||
break
|
||||
# log track on air
|
||||
@ -195,14 +197,14 @@ class Monitor:
|
||||
if self.sync_next and self.sync_next < now:
|
||||
return
|
||||
|
||||
self.sync_next = now + tz.timedelta(seconds = self.sync_timeout)
|
||||
self.sync_next = now + tz.timedelta(seconds=self.sync_timeout)
|
||||
|
||||
for source in self.station.sources:
|
||||
if source == self.station.dealer:
|
||||
continue
|
||||
playlist = source.program.sound_set.all() \
|
||||
.filter(type=Sound.Type.archive) \
|
||||
.values_list('path', flat = True)
|
||||
.values_list('path', flat=True)
|
||||
source.playlist = list(playlist)
|
||||
|
||||
def trace_canceled(self):
|
||||
@ -214,24 +216,24 @@ class Monitor:
|
||||
return
|
||||
|
||||
qs = Diffusions.objects.station(self.station).at().filter(
|
||||
type = Diffusion.Type.normal,
|
||||
sound__type = Sound.Type.archive,
|
||||
type=Diffusion.Type.normal,
|
||||
sound__type=Sound.Type.archive,
|
||||
)
|
||||
logs = Log.objects.station(station).on_air().with_diff()
|
||||
|
||||
date = tz.now() - datetime.timedelta(seconds = self.cancel_timeout)
|
||||
date = tz.now() - datetime.timedelta(seconds=self.cancel_timeout)
|
||||
for diff in qs:
|
||||
if logs.filter(diffusion = diff):
|
||||
if logs.filter(diffusion=diff):
|
||||
continue
|
||||
if diff.start < now:
|
||||
diff.type = Diffusion.Type.canceled
|
||||
diff.save()
|
||||
# log canceled diffusion
|
||||
self.log(
|
||||
type = Log.Type.other,
|
||||
diffusion = diff,
|
||||
comment = 'Diffusion canceled after {} seconds' \
|
||||
.format(self.cancel_timeout)
|
||||
type=Log.Type.other,
|
||||
diffusion=diff,
|
||||
comment='Diffusion canceled after {} seconds'
|
||||
.format(self.cancel_timeout)
|
||||
)
|
||||
|
||||
def __current_diff(self):
|
||||
@ -251,7 +253,7 @@ class Monitor:
|
||||
|
||||
# last sound source change: end of file reached or forced to stop
|
||||
sounds = Log.objects.station(station).on_air().with_sound() \
|
||||
.filter(date__gte = log.date) \
|
||||
.filter(date__gte=log.date) \
|
||||
.order_by('date')
|
||||
|
||||
if sounds.count() and sounds.last().source != log.source:
|
||||
@ -259,12 +261,12 @@ class Monitor:
|
||||
|
||||
# last diff is still playing: get remaining playlist
|
||||
sounds = sounds \
|
||||
.filter(source = log.source, pk__gt = log.pk) \
|
||||
.exclude(sound__type = Sound.Type.removed)
|
||||
.filter(source=log.source, pk__gt=log.pk) \
|
||||
.exclude(sound__type=Sound.Type.removed)
|
||||
|
||||
remaining = log.diffusion.get_sounds(archive = True) \
|
||||
.exclude(pk__in = sounds) \
|
||||
.values_list('path', flat = True)
|
||||
remaining = log.diffusion.get_sounds(archive=True) \
|
||||
.exclude(pk__in=sounds) \
|
||||
.values_list('path', flat=True)
|
||||
return log.diffusion, list(remaining)
|
||||
|
||||
def __next_diff(self, diff):
|
||||
@ -273,16 +275,14 @@ class Monitor:
|
||||
If diff is given, it is the one to be played right after it.
|
||||
"""
|
||||
station = self.station
|
||||
|
||||
kwargs = {'start__gte': diff.end } if diff else {}
|
||||
kwargs['type'] = Diffusion.Type.normal
|
||||
|
||||
qs = Diffusion.objects.station(station).at().filter(**kwargs) \
|
||||
kwargs = {'start__gte': diff.end} if diff else {}
|
||||
qs = Diffusion.objects.station(station) \
|
||||
.on_air().at().filter(**kwargs) \
|
||||
.distinct().order_by('start')
|
||||
diff = qs.first()
|
||||
return (diff, diff and diff.get_playlist(archive = True) or [])
|
||||
return (diff, diff and diff.get_playlist(archive=True) or [])
|
||||
|
||||
def handle_pl_sync(self, source, playlist, diff = None, date = None):
|
||||
def handle_pl_sync(self, source, playlist, diff=None, date=None):
|
||||
"""
|
||||
Update playlist of a source if required, and handle logging when
|
||||
it is needed.
|
||||
@ -297,11 +297,11 @@ class Monitor:
|
||||
source.playlist = playlist
|
||||
if diff and not diff.is_live():
|
||||
# log diffusion archive load
|
||||
self.log(type = Log.Type.load,
|
||||
source = source.id,
|
||||
diffusion = diff,
|
||||
date = date,
|
||||
comment = '\n'.join(playlist))
|
||||
self.log(type=Log.Type.load,
|
||||
source=source.id,
|
||||
diffusion=diff,
|
||||
date=date,
|
||||
comment='\n'.join(playlist))
|
||||
|
||||
def handle_diff_start(self, source, diff, date):
|
||||
"""
|
||||
@ -318,11 +318,11 @@ class Monitor:
|
||||
# live: just log it
|
||||
if diff.is_live():
|
||||
diff_ = Log.objects.station(self.station) \
|
||||
.filter(diffusion = diff, type = Log.Type.on_air)
|
||||
.filter(diffusion=diff, type=Log.Type.on_air)
|
||||
if not diff_.count():
|
||||
# log live diffusion
|
||||
self.log(type = Log.Type.on_air, source = source.id,
|
||||
diffusion = diff, date = date)
|
||||
self.log(type=Log.Type.on_air, source=source.id,
|
||||
diffusion=diff, date=date)
|
||||
return
|
||||
|
||||
# enable dealer
|
||||
@ -331,8 +331,8 @@ class Monitor:
|
||||
last_start = self.last_diff_start
|
||||
if not last_start or last_start.diffusion_id != diff.pk:
|
||||
# log triggered diffusion
|
||||
self.log(type = Log.Type.start, source = source.id,
|
||||
diffusion = diff, date = date)
|
||||
self.log(type=Log.Type.start, source=source.id,
|
||||
diffusion=diff, date=date)
|
||||
|
||||
def handle(self):
|
||||
"""
|
||||
@ -358,10 +358,10 @@ class Monitor:
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
help= __doc__
|
||||
help = __doc__
|
||||
|
||||
def add_arguments (self, parser):
|
||||
parser.formatter_class=RawTextHelpFormatter
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
group = parser.add_argument_group('actions')
|
||||
group.add_argument(
|
||||
'-c', '--config', action='store_true',
|
||||
@ -396,25 +396,25 @@ class Command (BaseCommand):
|
||||
'check'
|
||||
)
|
||||
|
||||
def handle (self, *args,
|
||||
config = None, run = None, monitor = None,
|
||||
station = [], delay = 1000, timeout = 600,
|
||||
**options):
|
||||
def handle(self, *args,
|
||||
config=None, run=None, monitor=None,
|
||||
station=[], delay=1000, timeout=600,
|
||||
**options):
|
||||
|
||||
stations = Station.objects.filter(name__in = station)[:] \
|
||||
if station else Station.objects.all()[:]
|
||||
stations = Station.objects.filter(name__in=station)[:] \
|
||||
if station else Station.objects.all()[:]
|
||||
|
||||
for station in stations:
|
||||
# station.prepare()
|
||||
if config and not run: # no need to write it twice
|
||||
if config and not run: # no need to write it twice
|
||||
station.streamer.push()
|
||||
if run:
|
||||
station.streamer.process_run()
|
||||
|
||||
if monitor:
|
||||
monitors = [
|
||||
Monitor(station, cancel_timeout = timeout)
|
||||
for station in stations
|
||||
Monitor(station, cancel_timeout=timeout)
|
||||
for station in stations
|
||||
]
|
||||
delay = delay / 1000
|
||||
while True:
|
||||
@ -425,4 +425,3 @@ class Command (BaseCommand):
|
||||
if run:
|
||||
for station in stations:
|
||||
station.controller.process_wait()
|
||||
|
||||
|
162
aircox/models.py
162
aircox/models.py
@ -11,6 +11,7 @@ from django.contrib.contenttypes.fields import (GenericForeignKey,
|
||||
GenericRelation)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils import timezone as tz
|
||||
@ -119,9 +120,7 @@ class Station(Nameable):
|
||||
|
||||
@property
|
||||
def outputs(self):
|
||||
"""
|
||||
Return all active output ports of the station
|
||||
"""
|
||||
""" Return all active output ports of the station """
|
||||
return self.port_set.filter(
|
||||
direction=Port.Direction.output,
|
||||
active=True,
|
||||
@ -129,9 +128,7 @@ class Station(Nameable):
|
||||
|
||||
@property
|
||||
def sources(self):
|
||||
"""
|
||||
Audio sources, dealer included
|
||||
"""
|
||||
""" Audio sources, dealer included """
|
||||
self.__prepare_controls()
|
||||
return self.__sources
|
||||
|
||||
@ -168,7 +165,7 @@ class Station(Nameable):
|
||||
|
||||
# FIXME can be a potential source of bug
|
||||
if date:
|
||||
date = utils.cast_date(date, to_datetime=False)
|
||||
date = utils.cast_date(date, datetime.date)
|
||||
if date and date > datetime.date.today():
|
||||
return []
|
||||
|
||||
@ -185,18 +182,17 @@ class Station(Nameable):
|
||||
start__lte=now) \
|
||||
.order_by('-start')[:count]
|
||||
|
||||
q = models.Q(diffusion__isnull=False) | \
|
||||
models.Q(track__isnull=False)
|
||||
q = Q(diffusion__isnull=False) | Q(track__isnull=False)
|
||||
logs = logs.station(self).on_air().filter(q).order_by('-date')
|
||||
|
||||
# filter out tracks played when there was a diffusion
|
||||
n, q = 0, models.Q()
|
||||
n, q = 0, Q()
|
||||
for diff in diffs:
|
||||
if count and n >= count:
|
||||
break
|
||||
# FIXME: does not catch tracks started before diff end but
|
||||
# that continued afterwards
|
||||
q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
|
||||
q = q | Q(date__gte=diff.start, date__lte=diff.end)
|
||||
n += 1
|
||||
logs = logs.exclude(q, diffusion__isnull=True)
|
||||
if count:
|
||||
@ -411,15 +407,11 @@ class Schedule(models.Model):
|
||||
Program, models.CASCADE,
|
||||
verbose_name=_('related program'),
|
||||
)
|
||||
time = models.TimeField(
|
||||
_('time'),
|
||||
blank=True, null=True,
|
||||
help_text=_('start time'),
|
||||
)
|
||||
date = models.DateField(
|
||||
_('date'),
|
||||
blank=True, null=True,
|
||||
help_text=_('date of the first diffusion'),
|
||||
_('date'), help_text=_('date of the first diffusion'),
|
||||
)
|
||||
time = models.TimeField(
|
||||
_('time'), help_text=_('start time'),
|
||||
)
|
||||
timezone = models.CharField(
|
||||
_('timezone'),
|
||||
@ -462,6 +454,12 @@ class Schedule(models.Model):
|
||||
|
||||
return pytz.timezone(self.timezone)
|
||||
|
||||
@property
|
||||
def datetime(self):
|
||||
""" Datetime for this schedule (timezone unaware) """
|
||||
import datetime
|
||||
return datetime.datetime.combine(self.date, self.time)
|
||||
|
||||
# initial cached data
|
||||
__initial = None
|
||||
|
||||
@ -481,22 +479,18 @@ class Schedule(models.Model):
|
||||
|
||||
def match(self, date=None, check_time=True):
|
||||
"""
|
||||
Return True if the given datetime matches the schedule
|
||||
Return True if the given date(time) matches the schedule.
|
||||
"""
|
||||
date = utils.date_or_default(date)
|
||||
date = utils.date_or_default(
|
||||
date, tz.datetime if check_time else datetime.date)
|
||||
|
||||
if self.date.weekday() != date.weekday() or \
|
||||
not self.match_week(date):
|
||||
|
||||
return False
|
||||
|
||||
if not check_time:
|
||||
return True
|
||||
|
||||
# we check against a normalized version (norm_date will have
|
||||
# schedule's date.
|
||||
|
||||
return date == self.normalize(date)
|
||||
return date == self.normalize(date) if check_time else True
|
||||
|
||||
def match_week(self, date=None):
|
||||
"""
|
||||
@ -509,15 +503,14 @@ class Schedule(models.Model):
|
||||
return False
|
||||
|
||||
# since we care only about the week, go to the same day of the week
|
||||
date = utils.date_or_default(date)
|
||||
date = utils.date_or_default(date, datetime.date)
|
||||
date += tz.timedelta(days=self.date.weekday() - date.weekday())
|
||||
|
||||
# FIXME this case
|
||||
|
||||
if self.frequency == Schedule.Frequency.one_on_two:
|
||||
# cf notes in date_of_month
|
||||
diff = utils.cast_date(date, False) - \
|
||||
utils.cast_date(self.date, False)
|
||||
diff = date - utils.cast_date(self.date, datetime.date)
|
||||
|
||||
return not (diff.days % 14)
|
||||
|
||||
@ -556,8 +549,7 @@ class Schedule(models.Model):
|
||||
return []
|
||||
|
||||
# first day of month
|
||||
date = utils.date_or_default(date, to_datetime=False) \
|
||||
.replace(day=1)
|
||||
date = utils.date_or_default(date, datetime.date).replace(day=1)
|
||||
freq = self.frequency
|
||||
|
||||
# last of the month
|
||||
@ -588,8 +580,8 @@ class Schedule(models.Model):
|
||||
|
||||
if freq == Schedule.Frequency.one_on_two:
|
||||
# check date base on a diff of dates base on a 14 days delta
|
||||
diff = utils.cast_date(date, False) - \
|
||||
utils.cast_date(self.date, False)
|
||||
diff = utils.cast_date(date, datetime.date) - \
|
||||
utils.cast_date(self.date, datetime.date)
|
||||
|
||||
if diff.days % 14:
|
||||
date += tz.timedelta(days=7)
|
||||
@ -636,13 +628,17 @@ class Schedule(models.Model):
|
||||
# new diffusions
|
||||
duration = utils.to_timedelta(self.duration)
|
||||
|
||||
delta = None
|
||||
if self.initial:
|
||||
delta = self.date - self.initial.date
|
||||
delta = self.datetime - self.initial.datetime
|
||||
|
||||
# FIXME: daylight saving bug: delta misses an hour when diffusion and
|
||||
# rerun are not on the same daylight-saving timezone
|
||||
diffusions += [
|
||||
Diffusion(
|
||||
program=self.program,
|
||||
type=Diffusion.Type.unconfirmed,
|
||||
initial=Diffusion.objects.filter(start=date - delta).first()
|
||||
initial=Diffusion.objects.program(self.program).filter(start=date-delta).first()
|
||||
if self.initial else None,
|
||||
start=date,
|
||||
end=date + duration,
|
||||
@ -685,7 +681,10 @@ class DiffusionQuerySet(models.QuerySet):
|
||||
def program(self, program):
|
||||
return self.filter(program=program)
|
||||
|
||||
def at(self, date=None, next=False, **kwargs):
|
||||
def on_air(self):
|
||||
return self.filter(type=Diffusion.Type.normal)
|
||||
|
||||
def at(self, date=None):
|
||||
"""
|
||||
Return diffusions occuring at the given date, ordered by +start
|
||||
|
||||
@ -694,12 +693,9 @@ class DiffusionQuerySet(models.QuerySet):
|
||||
it as a date, and get diffusions that occurs this day.
|
||||
|
||||
When date is None, uses tz.now().
|
||||
|
||||
When next is true, include diffusions that also occur after
|
||||
the given moment.
|
||||
"""
|
||||
# note: we work with localtime
|
||||
date = utils.date_or_default(date, keep_type=True)
|
||||
date = utils.date_or_default(date)
|
||||
|
||||
qs = self
|
||||
filters = None
|
||||
@ -708,43 +704,39 @@ class DiffusionQuerySet(models.QuerySet):
|
||||
# use datetime: we want diffusion that occurs around this
|
||||
# range
|
||||
filters = {'start__lte': date, 'end__gte': date}
|
||||
|
||||
if next:
|
||||
qs = qs.filter(
|
||||
models.Q(start__gte=date) | models.Q(**filters)
|
||||
)
|
||||
else:
|
||||
qs = qs.filter(**filters)
|
||||
qs = qs.filter(**filters)
|
||||
else:
|
||||
# use date: we want diffusions that occurs this day
|
||||
start, end = utils.date_range(date)
|
||||
filters = models.Q(start__gte=start, start__lte=end) | \
|
||||
models.Q(end__gt=start, end__lt=end)
|
||||
|
||||
if next:
|
||||
# include also diffusions of the next day
|
||||
filters |= models.Q(start__gte=start)
|
||||
qs = qs.filter(filters, **kwargs)
|
||||
|
||||
qs = qs.filter(Q(start__date=date) | Q(end__date=date))
|
||||
return qs.order_by('start').distinct()
|
||||
|
||||
def after(self, date=None, **kwargs):
|
||||
def after(self, date=None):
|
||||
"""
|
||||
Return a queryset of diffusions that happen after the given
|
||||
date.
|
||||
"""
|
||||
date = utils.date_or_default(date, keep_type=True)
|
||||
|
||||
return self.filter(start__gte=date, **kwargs).order_by('start')
|
||||
|
||||
def before(self, date=None, **kwargs):
|
||||
"""
|
||||
Return a queryset of diffusions that finish before the given
|
||||
date.
|
||||
date (default: today).
|
||||
"""
|
||||
date = utils.date_or_default(date)
|
||||
if isinstance(date, tz.datetime):
|
||||
qs = self.filter(start__gte=date)
|
||||
else:
|
||||
qs = self.filter(start__date__gte=date)
|
||||
return qs.order_by('start')
|
||||
|
||||
return self.filter(end__lte=date, **kwargs).order_by('start')
|
||||
def before(self, date=None):
|
||||
"""
|
||||
Return a queryset of diffusions that finish before the given
|
||||
date (default: today).
|
||||
"""
|
||||
date = utils.date_or_default(date)
|
||||
if isinstance(date, tz.datetime):
|
||||
qs = self.filter(start__lt=date)
|
||||
else:
|
||||
qs = self.filter(start__date__lt=date)
|
||||
return qs.order_by('start')
|
||||
|
||||
def range(self, start, end):
|
||||
# FIXME can return dates that are out of range...
|
||||
return self.after(start).before(end)
|
||||
|
||||
|
||||
class Diffusion(models.Model):
|
||||
@ -813,21 +805,19 @@ class Diffusion(models.Model):
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""
|
||||
Alias to self.start
|
||||
"""
|
||||
""" Return diffusion start as a date. """
|
||||
|
||||
return self.start
|
||||
return utils.cast_date(self.start)
|
||||
|
||||
@cached_property
|
||||
def local_date(self):
|
||||
def local_start(self):
|
||||
"""
|
||||
Return a version of self.date that is localized to self.timezone;
|
||||
This is needed since datetime are stored as UTC date and we want
|
||||
to get it as local time.
|
||||
"""
|
||||
|
||||
return tz.localtime(self.date, tz.get_current_timezone())
|
||||
return tz.localtime(self.start, tz.get_current_timezone())
|
||||
|
||||
@property
|
||||
def local_end(self):
|
||||
@ -892,10 +882,8 @@ class Diffusion(models.Model):
|
||||
"""
|
||||
|
||||
return Diffusion.objects.filter(
|
||||
models.Q(start__lt=self.start,
|
||||
end__gt=self.start) |
|
||||
models.Q(start__gt=self.start,
|
||||
start__lt=self.end)
|
||||
Q(start__lt=self.start, end__gt=self.start) |
|
||||
Q(start__gt=self.start, start__lt=self.end)
|
||||
).exclude(pk=self.pk).distinct()
|
||||
|
||||
def check_conflicts(self):
|
||||
@ -929,7 +917,7 @@ class Diffusion(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return '{self.program.name} {date} #{self.pk}'.format(
|
||||
self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
|
||||
self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -1298,11 +1286,8 @@ class LogQuerySet(models.QuerySet):
|
||||
return self.filter(station=station)
|
||||
|
||||
def at(self, date=None):
|
||||
start, end = utils.date_range(date)
|
||||
# return qs.filter(models.Q(end__gte = start) |
|
||||
# models.Q(date__lte = end))
|
||||
|
||||
return self.filter(date__gte=start, date__lte=end)
|
||||
date = utils.date_or_default(date)
|
||||
return self.filter(date__date=date)
|
||||
|
||||
def on_air(self):
|
||||
return self.filter(type=Log.Type.on_air)
|
||||
@ -1508,6 +1493,13 @@ class Log(models.Model):
|
||||
verbose_name=_('Track'),
|
||||
)
|
||||
|
||||
collision = models.ForeignKey(
|
||||
Diffusion, on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Collision'),
|
||||
related_name='+',
|
||||
)
|
||||
|
||||
objects = LogQuerySet.as_manager()
|
||||
|
||||
@property
|
||||
|
@ -4,68 +4,59 @@ import django.utils.timezone as tz
|
||||
|
||||
def date_range(date):
|
||||
"""
|
||||
Return a range of datetime for a given day, such as:
|
||||
[date, 0:0:0:0; date, 23:59:59:999]
|
||||
|
||||
Ensure timezone awareness.
|
||||
Return a datetime range for a given day, as:
|
||||
```(date, 0:0:0:0; date, 23:59:59:999)```.
|
||||
"""
|
||||
date = date_or_default(date)
|
||||
range = (
|
||||
date.replace(hour = 0, minute = 0, second = 0), \
|
||||
date.replace(hour = 23, minute = 59, second = 59, microsecond = 999)
|
||||
date = date_or_default(date, tz.datetime)
|
||||
return (
|
||||
date.replace(hour=0, minute=0, second=0),
|
||||
date.replace(hour=23, minute=59, second=59, microsecond=999)
|
||||
)
|
||||
return range
|
||||
|
||||
def cast_date(date, to_datetime = True):
|
||||
|
||||
def cast_date(date, into=datetime.date):
|
||||
"""
|
||||
Given a date reset its time information and
|
||||
return it as a date or datetime object.
|
||||
|
||||
Ensure timezone awareness.
|
||||
Cast a given date into the provided class' instance. Make datetime
|
||||
aware of timezone.
|
||||
"""
|
||||
if to_datetime:
|
||||
return tz.make_aware(
|
||||
tz.datetime(date.year, date.month, date.day, 0, 0, 0, 0)
|
||||
)
|
||||
return datetime.date(date.year, date.month, date.day)
|
||||
date = into(date.year, date.month, date.day)
|
||||
return tz.make_aware(date) if issubclass(into, tz.datetime) else date
|
||||
|
||||
def date_or_default(date, reset_time = False, keep_type = False, to_datetime = True):
|
||||
|
||||
def date_or_default(date, into=None):
|
||||
"""
|
||||
Return datetime or default value (now) if not defined, and remove time info
|
||||
if reset_time is True.
|
||||
|
||||
\param reset_time reset time info to 0
|
||||
\param keep_type keep the same type of the given date if not None
|
||||
\param to_datetime force conversion to datetime if not keep_type
|
||||
|
||||
Ensure timezone awareness.
|
||||
Return date if not None, otherwise return now. Cast result into provided
|
||||
type if any.
|
||||
"""
|
||||
date = date or tz.now()
|
||||
to_datetime = isinstance(date, tz.datetime) if keep_type else to_datetime
|
||||
date = date if date is not None else datetime.date.today() \
|
||||
if into is not None and issubclass(into, datetime.date) else \
|
||||
tz.datetime.now()
|
||||
|
||||
if reset_time or not isinstance(date, tz.datetime):
|
||||
return cast_date(date, to_datetime)
|
||||
if into is not None:
|
||||
date = cast_date(date, into)
|
||||
|
||||
if not tz.is_aware(date):
|
||||
if isinstance(date, tz.datetime) and not tz.is_aware(date):
|
||||
date = tz.make_aware(date)
|
||||
return date
|
||||
|
||||
def to_timedelta (time):
|
||||
|
||||
def to_timedelta(time):
|
||||
"""
|
||||
Transform a datetime or a time instance to a timedelta,
|
||||
only using time info
|
||||
"""
|
||||
return datetime.timedelta(
|
||||
hours = time.hour,
|
||||
minutes = time.minute,
|
||||
seconds = time.second
|
||||
hours=time.hour,
|
||||
minutes=time.minute,
|
||||
seconds=time.second
|
||||
)
|
||||
|
||||
def seconds_to_time (seconds):
|
||||
|
||||
def seconds_to_time(seconds):
|
||||
"""
|
||||
Seconds to datetime.time
|
||||
"""
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return datetime.time(hour = hours, minute = minutes, second = seconds)
|
||||
return datetime.time(hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
|
Reference in New Issue
Block a user