Merge branch 'streamer_live'

This commit is contained in:
bkfox 2017-06-30 14:18:56 +02:00
commit 013a0894ab
9 changed files with 243 additions and 235 deletions

View File

@ -183,8 +183,8 @@ class StationAdmin(admin.ModelAdmin):
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'related']
list_filter = ['date', 'source', 'related_type']
list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
admin.site.register(Port)

View File

@ -15,7 +15,14 @@ from django.conf import settings as main_settings
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone as tz
from aircox.models import Station, Diffusion, Track, Sound, Log
from aircox.models import Station, Diffusion, Track, Sound, Log #, DiffusionLog, SoundLog
class Tracer:
"""
Keep trace of played item and update logs in adequation to it
"""
pass
class Monitor:
@ -65,11 +72,12 @@ class Monitor:
self.sync_playlists()
self.handle()
def log(self, **kwargs):
def log(self, date = None, **kwargs):
"""
Create a log using **kwargs, and print info
"""
log = Log(station = self.station, **kwargs)
log = Log(station = self.station, date = date or tz.now(),
**kwargs)
log.save()
log.print()
@ -84,17 +92,18 @@ class Monitor:
if not current_sound or not current_source:
return
log = Log.objects.get_for(self.station, model = Sound) \
log = Log.objects.station(self.station, sound__isnull = False) \
.select_related('sound') \
.order_by('date').last()
# only streamed
if log and (log.related and not log.related.diffusion):
# only streamed ns
if log and not log.sound.diffusion:
self.trace_sound_tracks(log)
# TODO: expiration
if log and (log.source == current_source.id and \
log.related and
log.related.path == current_sound):
log.sound and
log.sound.path == current_sound):
return
sound = Sound.objects.filter(path = current_sound)
@ -102,7 +111,7 @@ class Monitor:
type = Log.Type.play,
source = current_source.id,
date = tz.now(),
related = sound[0] if sound else None,
sound = sound[0] if sound else None,
# keep sound path (if sound is removed, we keep that info)
comment = current_sound,
)
@ -112,11 +121,12 @@ class Monitor:
Log tracks for the given sound (for streamed programs); Called by
self.trace
"""
logs = Log.objects.get_for(self.station, model = Track) \
.filter(pk__gt = log.pk)
logs = [ log.related_id for log in logs ]
logs = Log.objects.station(self.station,
track__isnull = False,
pk__gt = log.pk) \
.values_list('sound__pk', flat = True)
tracks = Track.objects.get_for(object = log.related) \
tracks = Track.objects.get_for(object = log.sound) \
.filter(in_seconds = True)
if tracks and len(tracks) == len(logs):
return
@ -130,7 +140,7 @@ class Monitor:
type = Log.Type.play,
source = log.source,
date = pos,
related = track,
track = track,
comment = track,
)
@ -147,9 +157,9 @@ class Monitor:
for source in self.station.sources:
if source == self.station.dealer:
continue
playlist = [ sound.path for sound in
source.program.sound_set.all() ]
source.playlist = playlist
playlist = source.program.sound_set.all() \
.values_list('path', flat = True)
source.playlist = list(playlist)
def trace_canceled(self):
"""
@ -163,18 +173,18 @@ class Monitor:
type = Diffusion.Type.normal,
sound__type = Sound.Type.archive,
)
logs = station.played(models = Diffusion)
logs = station.played(diffusion__isnull = False)
date = tz.now() - datetime.timedelta(seconds = self.cancel_timeout)
for diff in diffs:
if logs.filter(related = diff):
if logs.filter(diffusion = diff):
continue
if diff.start < now:
diff.type = Diffusion.Type.canceled
diff.save()
self.log(
type = Log.Type.other,
related = diff,
diffusion = diff,
comment = 'Diffusion canceled after {} seconds' \
.format(self.cancel_timeout)
)
@ -187,53 +197,81 @@ class Monitor:
station = self.station
now = tz.now()
diff_log = station.played(models = Diffusion) \
.order_by('date').last()
if not diff_log or \
not diff_log.related.is_date_in_range(now):
log = station.played(diffusion__isnull = False) \
.select_related('diffusion') \
.order_by('date').last()
if not log or not log.diffusion.is_date_in_range(now):
# not running anymore
return None, []
# sound has switched? assume it has been (forced to) stopped
sounds = station.played(models = Sound) \
.filter(date__gte = diff_log.date) \
# last sound source change: end of file reached or forced to stop
sounds = station.played(sound__isnull = False) \
.filter(date__gte = log.date) \
.order_by('date')
if sounds.last() and sounds.last().source != diff_log.source:
return diff_log, []
if sounds.count() and sounds.last().source != log.source:
return None, []
# last diff is still playing: get the remaining playlist
sounds = sounds.filter(
source = diff_log.source, pk__gt = diff_log.pk
)
sounds = [
sound.related.path for sound in sounds
if sound.related.type != Sound.Type.removed
]
# last diff is still playing: get remaining playlist
sounds = sounds \
.filter(source = log.source, pk__gt = log.pk) \
.exclude(sound__type = Sound.Type.removed)
return (
diff_log.related,
[ path for path in diff_log.related.playlist
if path not in sounds ]
)
remaining = log.diffusion.get_archives().exclude(pk__in = sounds) \
.values_list('path', flat = True)
return log.diffusion, list(remaining)
def __next_diff(self, diff):
"""
Return the tuple with the next diff that should be played and
the playlist
Note: diff is a log
Return the next diffusion to be played as tuple of (diff, playlist).
If diff is given, it is the one to be played right after it.
"""
station = self.station
now = tz.now()
args = {'start__gt': diff.date } if diff else {}
diff = Diffusion.objects.at(station, now).filter(
type = Diffusion.Type.normal,
sound__type = Sound.Type.archive,
**args
).distinct().order_by('start').first()
kwargs = {'start__gte': diff.end } if diff else {}
diff = Diffusion.objects \
.at(station, now) \
.filter(type = Diffusion.Type.normal, **kwargs) \
.distinct().order_by('start')
diff = diff.first()
return (diff, diff and diff.playlist or [])
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.
"""
dealer = self.station.dealer
if dealer.playlist != playlist:
dealer.playlist = playlist
if diff and not diff.is_live():
self.log(type = Log.Type.load, source = source.id,
diffusion = diff, date = date)
def handle_diff_start(self, source, diff, date):
"""
Enable dealer in order to play a given diffusion if required,
handle start of diffusion
"""
if not diff or diff.start > date:
return
# live: just log it
if diff.is_live():
diff_ = Log.objects.station(self.station) \
.filter(diffusion = diff)
if not diff_.count():
self.log(type = Log.Type.on_air, source = source.id,
diffusion = diff, date = date)
return
# enable dealer
if not dealer.active:
dealer.active = True
self.log(type = Log.Type.play, source = source.id,
diffusion = diff, date = date)
def handle(self):
"""
Handle scheduled diffusion, trigger if needed, preload playlists
@ -246,33 +284,15 @@ class Monitor:
now = tz.now()
# current and next diffs
diff, playlist = self.__current_diff()
dealer.active = bool(playlist)
current_diff, remaining_pl = self.__current_diff()
next_diff, next_pl = self.__next_diff(current_diff)
next_diff, next_playlist = self.__next_diff(diff)
playlist += next_playlist
# playlist
dealer.active = bool(remaining_pl)
playlist = remaining_pl + next_pl
# playlist update
if dealer.playlist != playlist:
dealer.playlist = playlist
if next_diff:
self.log(
type = Log.Type.load,
source = dealer.id,
date = now,
related = next_diff
)
# dealer.on when next_diff start <= now
if next_diff and not dealer.active and \
next_diff.start <= now:
dealer.active = True
self.log(
type = Log.Type.play,
source = dealer.id,
date = now,
related = next_diff,
)
self.handle_pl_sync(dealer, playlist, next_diff, now)
self.handle_diff_start(dealer, next_diff, now)
class Command (BaseCommand):

View File

@ -50,7 +50,6 @@ class RelatedManager(models.Manager):
qs = qs.filter(related_id = object.pk)
return qs
class Related(models.Model):
"""
Add a field "related" of type GenericForeignKey, plus utilities.
@ -148,7 +147,6 @@ class Track(Related):
verbose_name = _('Track')
verbose_name_plural = _('Tracks')
#
# Station related classes
#
@ -180,7 +178,7 @@ class Station(Nameable):
__dealer = None
__streamer = None
def __prepare(self):
def __prepare_controls(self):
import aircox.controllers as controllers
if not self.__streamer:
self.__streamer = controllers.Streamer(station = self)
@ -215,12 +213,12 @@ class Station(Nameable):
"""
Audio sources, dealer included
"""
self.__prepare()
self.__prepare_controls()
return self.__sources
@property
def dealer(self):
self.__prepare()
self.__prepare_controls()
return self.__dealer
@property
@ -228,86 +226,25 @@ class Station(Nameable):
"""
Audio controller for the station
"""
self.__prepare()
self.__prepare_controls()
return self.__streamer
def played(self, models, archives = True):
def played(self, *args, **kwargs):
"""
Call Log.objects.played for this station
"""
return Log.objects.played(self, models, archives)
@staticmethod
def __mix_logs_and_diff(diffs, logs, count = 0):
"""
Mix together logs and diffusion items of the same day,
ordered by their date.
Diffs and Logs are assumed to be ordered by -date, and so is
the resulting list
"""
# we fill a list with diff and retrieve logs that happened between
# each to put them too there.
# we do the algorithm in the reverse way in order to be able to limit
# process calculations using count if needed.
diff_ = None
now = tz.now()
items = []
logs = logs.order_by('-date')
for diff in diffs.order_by('-start'):
if diff_:
logs_ = logs.filter(date__gt = diff.end, date__lt = diff_.start)
else:
logs_ = logs.filter(date__gt = diff.end)
if diff.end < now:
# a log can be started before the end of the diffusion and still
# is running. We can't say if it has been properly finished
# before the end of the diffusion, but we assume that in most
# cases this is true.
# We just check if there is some other log after this partial
# one.
partial = logs.filter(
date__gt = diff.start, date__lt = diff.end
).last()
if partial:
next_log = logs.filter(pk__gt = partial.pk).first()
if not next_log or next_log.date > diff.end:
partial.date = diff.end
logs_ = list(logs_) + [partial]
# append to list
diff_ = diff
items.extend(logs_)
items.append(diff)
if count and len(items) >= count:
break
if diff_:
if count and len(items) >= count:
return items[:count]
logs_ = logs.filter(date__lt = diff_.start)
else:
logs_ = logs.all()
items.extend(logs_)
return items[:count] if count else items
return Log.objects.played(self, *args, **kwargs)
def on_air(self, date = None, count = 0):
"""
Return a list of what happened on air, based on logs and
diffusions informations. The list is sorted by -date.
Return a queryset of what happened on air, based on logs and
diffusions informations. The queryset is sorted by -date.
* date: only for what happened on this date;
* count: number of items to retrieve if not zero;
If date is not specified, count MUST be set to a non-zero value.
Be careful with what you which for: the result is a plain list.
The list contains:
* track logs: for the streamed programs;
* diffusion: for the scheduled diffusions;
"""
# FIXME: as an iterator?
# TODO argument to get sound instead of tracks
@ -315,21 +252,39 @@ class Station(Nameable):
raise ValueError('at least one argument must be set')
# FIXME can be a potential source of bug
if date:
date = utils.cast_date(date, to_datetime = False)
if date and date > datetime.date.today():
return []
if date:
logs = Log.objects.at_for(self, date, model = Track)
diffs = Diffusion.objects.at(self, date)
logs = Log.objects.at(self, date)
diffs = Diffusion.objects.at(self, date, type = Diffusion.Type.normal) \
.order_by('-start')
else:
logs = Log.objects.get_for(self, model = Track)
diffs = Diffusion.objects
logs = logs.filter(station = self)
logs = Log.objects
diffs = Diffusion.objects.filter(type = Diffusion.Type.normal,
start__lte = tz.now()) \
.order_by('-start')[:count]
diffs = diffs.filter(program__station = self) \
.filter(type = Diffusion.Type.normal) \
.filter(start__lte = tz.now())
return self.__mix_logs_and_diff(diffs, logs, count)
q = models.Q(diffusion__isnull = False) | \
models.Q(track__isnull = False)
logs = logs.filter(q).order_by('-date')
# filter out tracks played when there was a diffusion
n = 0
q = models.Q()
for diff in diffs:
if count and n >= count:
break
q = q | models.Q(date__gte = diff.start, date__lte = diff.end)
n += 1
logs = logs.exclude(q, diffusion__isnull = True)
if count:
logs = logs[:count]
return logs
def save(self, make_sources = True, *args, **kwargs):
if not self.path:
@ -348,9 +303,9 @@ class Station(Nameable):
class ProgramManager(models.Manager):
def station(self, station, qs = None):
def station(self, station, qs = None, **kwargs):
qs = self if qs is None else qs
return qs.filter(station = station)
return qs.filter(station = station, **kwargs)
class Program(Nameable):
"""
@ -730,11 +685,11 @@ class Schedule(models.Model):
class DiffusionManager(models.Manager):
def station(self, station, qs = None):
def station(self, station, qs = None, **kwargs):
qs = self if qs is None else qs
return qs.filter(program__station = station)
return qs.filter(program__station = station, **kwargs)
def at(self, station, date = None, next = False, qs = None):
def at(self, station, date = None, next = False, qs = None, **kwargs):
"""
Return diffusions occuring at the given date, ordered by +start
@ -770,7 +725,7 @@ class DiffusionManager(models.Manager):
if next:
# include also diffusions of the next day
filters |= models.Q(start__gte = start)
qs = qs.filter(filters)
qs = qs.filter(filters, **kwargs)
return self.station(station, qs).order_by('start').distinct()
def after(self, station, date = None, qs = None):
@ -867,7 +822,12 @@ class Diffusion(models.Model):
"""
List of archives' path; uses get_archives
"""
return [ sound.path for sound in self.get_archives() ]
playlist = self.get_archives().values_list('path', flat = True)
return list(playlist)
def is_live(self):
return self.type == self.Type.normal and \
not self.get_archives().count()
def get_archives(self):
"""
@ -1229,59 +1189,51 @@ class Port (models.Model):
)
class LogManager(RelatedManager):
def station(self, station, qs = None):
class LogManager(models.Manager):
def station(self, station, qs = None, **kwargs):
qs = self if qs is None else qs
return qs.filter(station = station)
return qs.filter(station = station, **kwargs)
def get_for(self, station, *args, **kwargs):
qs = super().get_for(*args, **kwargs)
return self.station(station, qs) if station else qs
def _at(self, date = None, qs = None):
def _at(self, date = None, qs = None, **kwargs):
start, end = utils.date_range(date)
qs = self if qs is None else qs
return qs.filter(date__gte = start,
date__lte = end)
return qs.filter(date__gte = start, date__lte = end, **kwargs)
def at(self, station = None, date = None, qs = None):
def at(self, station = None, date = None, qs = None, **kwargs):
"""
Return a queryset of logs that have the given date
in their range.
"""
qs = self._at(date, qs)
qs = self._at(date, qs, **kwargs)
return self.station(station, qs) if station else qs
def at_for(self, station, date, object = None, model = None, qs = None):
"""
Return a queryset of logs that occured at the given date
for the given model or object.
"""
qs = self.get_for(station, object, model, qs)
return self._at(date, qs)
def played(self, station, models, archives = True):
def played(self, station, archives = True, include_live = True,
**kwargs):
"""
Return a queryset of the played elements' log for the given
station and model. This queryset is ordered by date ascending
* station: related station
* models: a model or a list of models
* archives: if false, exclude log of diffusion's archives from
the queryset;
* include_live: include diffusion that have no archive
* kwargs: extra filter kwargs
"""
qs = self.get_for(station, model = models) \
.filter(type = Log.Type.play)
if include_live:
qs = self.filter(type__in = (Log.Type.play, Log.Type.on_air),
**kwargs)
else:
qs = self.filter(type = Log.Type.play, **kwargs)
if not archives and station.dealer:
qs = qs.exclude(
source = station.dealer.id,
related_type = ContentType.objects.get_for_model(Sound)
sound__isnull = False
)
return qs.order_by('date')
class Log(Related):
class Log(models.Model):
"""
Log sounds and diffusions that are played on the station.
@ -1303,14 +1255,18 @@ class Log(Related):
"""
Source starts to be preload related_object
"""
other = 0x03
on_air = 0x03
"""
A diffusion occured, but in live (no sound played by Aircox)
"""
other = 0x04
"""
Other log
"""
type = models.SmallIntegerField(
verbose_name = _('type'),
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
choices = [ (int(y), _(x.replace('_',' '))) for x,y in Type.__members__.items() ],
blank = True, null = True,
)
station = models.ForeignKey(
@ -1329,6 +1285,7 @@ class Log(Related):
date = models.DateTimeField(
_('date'),
default=tz.now,
db_index = True,
)
comment = models.CharField(
_('comment'),
@ -1336,6 +1293,25 @@ class Log(Related):
blank = True, null = True,
)
diffusion = models.ForeignKey(
Diffusion,
verbose_name = _('Diffusion'),
blank = True, null = True,
db_index = True,
)
sound = models.ForeignKey(
Sound,
verbose_name = _('Sound'),
blank = True, null = True,
db_index = True,
)
track = models.ForeignKey(
Track,
verbose_name = _('Track'),
blank = True, null = True,
db_index = True,
)
objects = LogManager()
@property
@ -1343,12 +1319,16 @@ class Log(Related):
"""
Calculated end using self.related informations
"""
if self.related_type == Diffusion:
return self.related.end
if self.related_type == Sound:
return self.date + to_timedelta(self.duration)
if self.diffusion:
return self.diffusion.end
if self.sound:
return self.date + to_timedelta(sound.duration)
return self.date
@property
def related(self):
return self.diffusion or self.sound or self.track
def is_expired(self, date = None):
"""
Return True if the log is expired. Note that it only check
@ -1359,11 +1339,19 @@ class Log(Related):
return self.end < date
def print(self):
r = []
if self.diffusion:
r.append('diff: ' + str(self.diffusion_id))
if self.sound:
r.append('sound: ' + str(self.sound_id))
if self.track:
r.append('track: ' + str(self.track_id))
logger.info('log #%s: %s%s',
str(self),
self.comment or '',
' -- {} #{}'.format(self.related_type, self.related_id)
if self.related else ''
' (' + ', '.join(r) + ')' if r else ''
)
def __str__(self):
@ -1371,4 +1359,3 @@ class Log(Related):
self.pk, self.date.strftime('%Y/%m/%d %H:%M'), self.source
)

View File

@ -40,12 +40,12 @@ def on_air(request):
else:
station = stations.stations.first()
last = station.on_air(count = 10)
if not last:
on_air = station.on_air(count = 10).select_related('track','diffusion')
if not on_air.count():
return HttpResponse('')
last = last[0]
if type(last) == models.Log:
last = on_air.last()
if last.track:
last = {
'type': 'track',
'artist': last.related.artist,
@ -54,11 +54,12 @@ def on_air(request):
}
else:
try:
diff = last.diffusion
publication = None
if cms:
publication = \
cms.DiffusionPage.objects.filter(
diffusion = last.initial or last).first() or \
diffusion = diff.initial or diff).first() or \
cms.ProgramPage.objects.filter(
program = last.program).first()
except:
@ -66,8 +67,8 @@ def on_air(request):
last = {
'type': 'diffusion',
'title': last.program.name,
'date': last.start,
'title': diff.program.name,
'date': diff.start,
'url': publication.specific.url if publication else None,
}

View File

@ -64,7 +64,7 @@ class Command (BaseCommand):
initial__isnull = True
).exclude(type = Diffusion.Type.unconfirmed)
for diffusion in qs:
if not diffusion.program.page.count():
if not diffusion.program.page:
if not hasattr(diffusion.program, '__logged_diff_error'):
logger.warning(
'the program {} has no page; skip the creation of '
@ -80,7 +80,7 @@ class Command (BaseCommand):
page = DiffusionPage.from_diffusion(
diffusion, live = False
)
diffusion.program.page.first().add_child(instance = page)
diffusion.program.page.add_child(instance = page)
except:
import sys
e = sys.exc_info()[0]

View File

@ -452,10 +452,8 @@ class ProgramPage(Publication):
def diffs_to_page(self, diffs):
for diff in diffs:
if diff.page.count():
diff.page_ = diff.page.first()
else:
diff.page_ = ListItem(
if not diff.page:
diff.page = ListItem(
title = '{}, {}'.format(
self.program.name, diff.date.strftime('%d %B %Y')
),
@ -464,7 +462,7 @@ class ProgramPage(Publication):
date = diff.start,
)
return [
diff.page_ for diff in diffs if diff.page_.live
diff.page for diff in diffs if diff.page.live
]
@property
@ -560,8 +558,8 @@ class DiffusionPage(Publication):
'title': '{}, {}'.format(
diff.program.name, tz.localtime(diff.date).strftime('%d %B %Y')
),
'cover': (diff.program.page.count() and \
diff.program.page.first().cover) or None,
'cover': (diff.program.page and \
diff.program.page.cover) or None,
'date': diff.start,
}
model_kwargs.update(kwargs)
@ -637,7 +635,7 @@ class DiffusionPage(Publication):
if self.diffusion:
# set publish_as
if not self.pk:
self.publish_as = self.diffusion.program.page.first()
self.publish_as = self.diffusion.program.page
# sync date
self.date = self.diffusion.start
@ -777,8 +775,9 @@ class LogsPage(DatedListPage):
logs = []
for date in context['nav_dates']['dates']:
items = [ SectionLogsList.as_item(item)
for item in self.station.on_air(date = date) ]
items = self.station.on_air(date = date) \
.select_related('track','diffusion')
items = [ SectionLogsList.as_item(item) for item in items ]
logs.append(
(date, reversed(items) if self.reverse else items)
)

View File

@ -957,16 +957,16 @@ class SectionLogsList(SectionItem):
Supports: Log/Track, Diffusion
"""
from aircox_cms.models import DiffusionPage
if type(log) == aircox.models.Diffusion:
return DiffusionPage.as_item(log)
if log.diffusion:
return DiffusionPage.as_item(log.diffusion)
related = log.related
track = log.track
return ListItem(
title = '{artist} -- {title}'.format(
artist = related.artist,
title = related.title,
artist = track.artist,
title = track.title,
),
headline = related.info,
headline = track.info,
date = log.date,
info = '',
css_class = 'track'

View File

@ -118,7 +118,7 @@ def station_post_saved(sender, instance, created, *args, **kwargs):
@receiver(post_save, sender=aircox.Program)
def program_post_saved(sender, instance, created, *args, **kwargs):
if not created or instance.page.count():
if not created or instance.page:
return
settings = utils.get_station_settings(instance.station)
@ -191,7 +191,7 @@ def diffusion_post_saved(sender, instance, created, *args, **kwargs):
page = models.DiffusionPage.from_diffusion(
instance, live = False
)
instance.program.page.first().add_child(
instance.program.page.add_child(
instance = page
)

View File

@ -127,8 +127,8 @@ class LogAdmin(ModelAdmin):
menu_label = _('Logs')
menu_icon = 'time'
menu_order = 300
list_display = ['date', 'station', 'source', 'type', 'comment', 'related']
list_filter = ['date', 'source', 'related_type']
list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
aircox.models.Log.panels = [
MultiFieldPanel([
@ -139,11 +139,12 @@ aircox.models.Log.panels = [
]),
FieldPanel('type'),
FieldPanel('comment'),
FieldRowPanel([
FieldPanel('related_type'),
FieldPanel('related_id')
]),
], heading = _('Log')),
MultiFieldPanel([
FieldPanel('diffusion'),
FieldPanel('sound'),
FieldPanel('track'),
], heading = _('Related objects')),
]
@ -327,7 +328,7 @@ class TodayMenu(GenericMenu):
return MenuItem(label, self.page_url(item), attrs = attrs)
def get_parent(self, item):
return item.program.page.first()
return item.program.page
@hooks.register('register_admin_menu_item')