fix bug in streamer; clean-up; wider sidebar
This commit is contained in:
parent
09859c9410
commit
c059a33077
|
@ -51,6 +51,9 @@ class TrackInline(GenericTabularInline):
|
|||
extra = 0
|
||||
fields = ('artist', 'title', 'info', 'position', 'in_seconds', 'tags')
|
||||
|
||||
list_display = ['artist','title','tags','related']
|
||||
list_filter = ['artist','title','tags']
|
||||
|
||||
|
||||
@admin.register(Sound)
|
||||
class SoundAdmin(NameableAdmin):
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import os
|
||||
import signal
|
||||
import re
|
||||
import subprocess
|
||||
import atexit
|
||||
import logging
|
||||
import atexit, logging, os, re, signal, subprocess
|
||||
|
||||
import tzlocal
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone as tz
|
||||
|
||||
import aircox.models as models
|
||||
import aircox.settings as settings
|
||||
|
||||
from aircox.connector import Connector
|
||||
|
||||
|
||||
local_tz = tzlocal.get_localzone()
|
||||
logger = logging.getLogger('aircox.tools')
|
||||
|
||||
|
||||
|
@ -32,19 +32,14 @@ class Streamer:
|
|||
"""
|
||||
Path of the configuration file.
|
||||
"""
|
||||
current_sound = ''
|
||||
source = None
|
||||
"""
|
||||
Current sound being played (retrieved by fetch)
|
||||
"""
|
||||
current_source = None
|
||||
"""
|
||||
Current source object that is responsible of self.current_sound
|
||||
Current source object that is responsible of self.sound
|
||||
"""
|
||||
process = None
|
||||
"""
|
||||
Application's process if ran from Streamer
|
||||
"""
|
||||
|
||||
socket_path = ''
|
||||
"""
|
||||
Path to the connector's socket
|
||||
|
@ -95,13 +90,11 @@ class Streamer:
|
|||
if not data:
|
||||
return
|
||||
|
||||
self.current_sound = data.get('initial_uri')
|
||||
self.current_source = next(
|
||||
self.source = next(
|
||||
iter(source for source in self.station.sources
|
||||
if source.rid == rid),
|
||||
self.current_source
|
||||
self.source
|
||||
)
|
||||
self.current_source.metadata = data
|
||||
|
||||
def push(self, config = True):
|
||||
"""
|
||||
|
@ -202,43 +195,25 @@ class Source:
|
|||
Controller of a Source. Value are usually updated directly on the
|
||||
external side.
|
||||
"""
|
||||
program = None
|
||||
"""
|
||||
Related source
|
||||
"""
|
||||
name = ''
|
||||
|
||||
path = ''
|
||||
"""
|
||||
Path to the Source's playlist file. Optional.
|
||||
"""
|
||||
active = True
|
||||
"""
|
||||
Source is available. May be different from the containing Source,
|
||||
e.g. dealer and liquidsoap.
|
||||
"""
|
||||
current_sound = ''
|
||||
"""
|
||||
Current sound being played (retrieved by fetch)
|
||||
"""
|
||||
current_source = None
|
||||
"""
|
||||
Current source being responsible of the current sound
|
||||
"""
|
||||
|
||||
rid = None
|
||||
"""
|
||||
Current request id of the source in LiquidSoap
|
||||
"""
|
||||
station = None
|
||||
connector = None
|
||||
"""
|
||||
Connector to Liquidsoap server
|
||||
"""
|
||||
metadata = None
|
||||
"""
|
||||
Dict of file's metadata given by Liquidsoap. Set by Stream when
|
||||
fetch()ing
|
||||
"""
|
||||
""" Connector to Liquidsoap server """
|
||||
program = None
|
||||
""" Related program """
|
||||
name = ''
|
||||
""" Name of the source """
|
||||
path = ''
|
||||
""" Path to the playlist file. """
|
||||
on_air = None
|
||||
|
||||
|
||||
# retrieved from fetch
|
||||
sound = ''
|
||||
""" (fetched) current sound being played """
|
||||
rid = None
|
||||
""" (fetched) current request id of the source in LiquidSoap """
|
||||
air_time = None
|
||||
""" (fetched) datetime of last on_air """
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
|
@ -267,12 +242,6 @@ class Source:
|
|||
if not self.__playlist:
|
||||
self.from_db()
|
||||
|
||||
def is_stream(self):
|
||||
return self.program and not self.program.show
|
||||
|
||||
def is_dealer(self):
|
||||
return not self.program
|
||||
|
||||
@property
|
||||
def playlist(self):
|
||||
"""
|
||||
|
@ -325,11 +294,19 @@ class Source:
|
|||
if self.__playlist else []
|
||||
|
||||
#
|
||||
# RPC
|
||||
# RPC & States
|
||||
#
|
||||
def _send(self, *args, **kwargs):
|
||||
return self.connector.send(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_stream(self):
|
||||
return self.program and not self.program.show
|
||||
|
||||
@property
|
||||
def is_dealer(self):
|
||||
return not self.program
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
return self._send('var.get ', self.id, '_active') == 'true'
|
||||
|
@ -348,9 +325,15 @@ class Source:
|
|||
return
|
||||
|
||||
self.rid = data.get('rid')
|
||||
self.current_sound = data.get('initial_uri')
|
||||
self.sound = data.get('initial_uri')
|
||||
|
||||
# TODO: get metadata
|
||||
# get air_time
|
||||
air_time = data.get('on_air')
|
||||
# try:
|
||||
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
||||
self.air_time = local_tz.localize(air_time)
|
||||
# except:
|
||||
# pass
|
||||
|
||||
def push(self):
|
||||
"""
|
||||
|
@ -383,8 +366,9 @@ class Source:
|
|||
|
||||
def stream(self):
|
||||
"""
|
||||
Return a dict with stream info for a Stream program, or None if there
|
||||
is not. Used in the template.
|
||||
Return dict of info for the current Stream program running on
|
||||
the source. If not, return None.
|
||||
[ used in the templates ]
|
||||
"""
|
||||
# TODO: multiple streams
|
||||
stream = self.program.stream_set.all().first()
|
||||
|
|
|
@ -16,8 +16,9 @@ from django.core.management.base import BaseCommand, CommandError
|
|||
from django.utils import timezone as tz
|
||||
from django.utils.functional import cached_property
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
from aircox.models import Station, Diffusion, Track, Sound, Log #, DiffusionLog, SoundLog
|
||||
from aircox.models import Station, Diffusion, Track, Sound, Log
|
||||
|
||||
# force using UTC
|
||||
import pytz
|
||||
|
@ -61,17 +62,20 @@ class Monitor:
|
|||
"""
|
||||
|
||||
def get_last_log(self, *args, **kwargs):
|
||||
return self.log_qs.filter(*args, **kwargs).last()
|
||||
|
||||
@property
|
||||
def log_qs(self):
|
||||
return Log.objects.station(self.station) \
|
||||
.filter(*args, **kwargs) \
|
||||
.select_related('diffusion', 'sound') \
|
||||
.order_by('pk').last()
|
||||
.order_by('pk')
|
||||
|
||||
@property
|
||||
def last_log(self):
|
||||
"""
|
||||
Last log of monitored station
|
||||
"""
|
||||
return self.get_last_log()
|
||||
return self.log_qs.last()
|
||||
|
||||
@property
|
||||
def last_sound(self):
|
||||
|
@ -104,7 +108,15 @@ class Monitor:
|
|||
if not self.streamer.ready():
|
||||
return
|
||||
|
||||
self.trace()
|
||||
self.streamer.fetch()
|
||||
source = self.streamer.source
|
||||
if source and source.sound:
|
||||
log = self.trace_sound(source)
|
||||
if log:
|
||||
self.trace_tracks(log)
|
||||
else:
|
||||
print('no source or sound for stream; source = ', source)
|
||||
|
||||
self.sync_playlists()
|
||||
self.handle()
|
||||
|
||||
|
@ -116,92 +128,57 @@ class Monitor:
|
|||
**kwargs)
|
||||
log.save()
|
||||
log.print()
|
||||
|
||||
# update last log
|
||||
if log.type != Log.Type.other and \
|
||||
self.last_log and not self.last_log.end:
|
||||
self.last_log.end = log.date
|
||||
return log
|
||||
|
||||
def trace(self):
|
||||
def trace_sound(self, source):
|
||||
"""
|
||||
Check the current_sound of the station and update logs if
|
||||
needed.
|
||||
Return log for current on_air (create and save it if required).
|
||||
"""
|
||||
self.streamer.fetch()
|
||||
current_sound = self.streamer.current_sound
|
||||
current_source = self.streamer.current_source
|
||||
if not current_sound or not current_source:
|
||||
print('no source / no sound', current_sound, current_source)
|
||||
return
|
||||
sound_path = source.sound
|
||||
air_time = source.air_time
|
||||
|
||||
log = self.get_last_log(
|
||||
models.Q(sound__isnull = False) |
|
||||
models.Q(diffusion__isnull = False),
|
||||
type = Log.Type.on_air
|
||||
# check if there is yet a log for this sound on the source
|
||||
delta = tz.timedelta(seconds=5)
|
||||
air_times = (air_time - delta, air_time + delta)
|
||||
|
||||
log = self.log_qs.on_air().filter(
|
||||
source = source.id, sound__path = sound_path,
|
||||
date__range = air_times,
|
||||
).last()
|
||||
if log:
|
||||
return log
|
||||
|
||||
# get sound
|
||||
sound = Sound.objects.filter(path = sound_path) \
|
||||
.select_related('diffusion').first()
|
||||
diff = None
|
||||
if sound and sound.diffusion:
|
||||
diff = sound.diffusion.original
|
||||
# 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()
|
||||
|
||||
# log sound on air
|
||||
return self.log(
|
||||
type = Log.Type.on_air,
|
||||
source = source.id,
|
||||
date = source.on_air,
|
||||
sound = sound,
|
||||
diffusion = diff,
|
||||
# if sound is removed, we keep sound path info
|
||||
comment = sound_path,
|
||||
)
|
||||
|
||||
on_air = None
|
||||
if log:
|
||||
# we always check difference in sound info
|
||||
is_diff = log.source != current_source.id or \
|
||||
(log.sound and log.sound.path != current_sound)
|
||||
|
||||
# check if sound 'on air' time has changed compared to logged one.
|
||||
# in some cases, there can be a gap between liquidsoap on_air and
|
||||
# log's date; to avoid duplicate we allow a difference of 5 seconds
|
||||
if not is_diff:
|
||||
try:
|
||||
# FIXME: liquidsoap does not have timezone
|
||||
on_air = current_source.metadata and \
|
||||
current_source.metadata.get('on_air')
|
||||
on_air = tz.datetime.strptime(on_air, "%Y/%m/%d %H:%M:%S")
|
||||
on_air = local_tz.localize(on_air)
|
||||
on_air = on_air.astimezone(pytz.utc)
|
||||
|
||||
is_diff = is_diff or ((log.date - on_air).total_seconds() > 5)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
# no log: sound is different
|
||||
is_diff = True
|
||||
|
||||
if is_diff:
|
||||
sound = Sound.objects.filter(path = current_sound).first()
|
||||
|
||||
# find an eventual diffusion associated to current sound
|
||||
# => check using last (started) diffusion's archives
|
||||
last_diff = self.last_diff_start
|
||||
diff = None
|
||||
if last_diff and not last_diff.is_expired():
|
||||
archives = last_diff.diffusion.sounds(archive = True)
|
||||
if archives.filter(pk = sound.pk).exists():
|
||||
diff = last_diff.diffusion
|
||||
|
||||
# log sound on air
|
||||
log = self.log(
|
||||
type = Log.Type.on_air,
|
||||
source = current_source.id,
|
||||
date = on_air or tz.now(),
|
||||
sound = sound,
|
||||
diffusion = diff,
|
||||
# if sound is removed, we keep sound path info
|
||||
comment = current_sound,
|
||||
)
|
||||
|
||||
# trace tracks
|
||||
self.trace_sound_tracks(log)
|
||||
|
||||
|
||||
def trace_sound_tracks(self, log):
|
||||
def trace_tracks(self, log):
|
||||
"""
|
||||
Log tracks for the given sound log (for streamed programs only).
|
||||
Called by self.trace
|
||||
"""
|
||||
if log.diffusion:
|
||||
return
|
||||
|
||||
tracks = Track.objects.get_for(object = log.sound) \
|
||||
tracks = Track.objects.related(object = log.sound) \
|
||||
.filter(in_seconds = True)
|
||||
if not tracks.exists():
|
||||
return
|
||||
|
@ -249,7 +226,7 @@ class Monitor:
|
|||
type = Diffusion.Type.normal,
|
||||
sound__type = Sound.Type.archive,
|
||||
)
|
||||
logs = station.raw_on_air(diffusion__isnull = False)
|
||||
logs = Log.objects.station(station).on_air().with_diff()
|
||||
|
||||
date = tz.now() - datetime.timedelta(seconds = self.cancel_timeout)
|
||||
for diff in qs:
|
||||
|
@ -274,17 +251,17 @@ class Monitor:
|
|||
station = self.station
|
||||
now = tz.now()
|
||||
|
||||
log = station.raw_on_air(diffusion__isnull = False) \
|
||||
.select_related('diffusion') \
|
||||
.order_by('date').last()
|
||||
log = Log.objects.station(station).on_air().with_diff() \
|
||||
.select_related('diffusion') \
|
||||
.order_by('date').last()
|
||||
if not log or not log.diffusion.is_date_in_range(now):
|
||||
# not running anymore
|
||||
return None, []
|
||||
|
||||
# last sound source change: end of file reached or forced to stop
|
||||
sounds = station.raw_on_air(sound__isnull = False) \
|
||||
.filter(date__gte = log.date) \
|
||||
.order_by('date')
|
||||
sounds = Log.objects.station(station).on_air().with_sound() \
|
||||
.filter(date__gte = log.date) \
|
||||
.order_by('date')
|
||||
|
||||
if sounds.count() and sounds.last().source != log.source:
|
||||
return None, []
|
||||
|
@ -294,7 +271,7 @@ class Monitor:
|
|||
.filter(source = log.source, pk__gt = log.pk) \
|
||||
.exclude(sound__type = Sound.Type.removed)
|
||||
|
||||
remaining = log.diffusion.sounds(archive = True) \
|
||||
remaining = log.diffusion.get_sounds(archive = True) \
|
||||
.exclude(pk__in = sounds) \
|
||||
.values_list('path', flat = True)
|
||||
return log.diffusion, list(remaining)
|
||||
|
|
|
@ -28,17 +28,15 @@ class AircoxMiddleware(object):
|
|||
This middleware must be set after the middleware
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
"""
|
||||
default_qs = models.Station.objects.filter(default = True)
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def init_station(self, request, aircox):
|
||||
# update current station
|
||||
|
||||
def update_station(self, request):
|
||||
station = request.GET.get('aircox.station')
|
||||
pk = None
|
||||
try:
|
||||
if station:
|
||||
if station is not None:
|
||||
pk = request.GET['aircox.station']
|
||||
if station:
|
||||
pk = int(pk)
|
||||
|
@ -47,28 +45,23 @@ class AircoxMiddleware(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
# select current station
|
||||
station = None
|
||||
pk = None
|
||||
def init_station(self, request, aircox):
|
||||
self.update_station(request)
|
||||
|
||||
try:
|
||||
pk = request.session.get('aircox.station')
|
||||
if pk:
|
||||
pk = int(pk)
|
||||
station = models.Station.objects.filter(pk = pk).first()
|
||||
pk = int(pk) if pk else None
|
||||
except:
|
||||
pass
|
||||
|
||||
if not station:
|
||||
pk = None
|
||||
station = self.default_qs.first() or \
|
||||
models.Station.objects.first()
|
||||
|
||||
aircox.station = station
|
||||
aircox.station = models.Station.objects.default(pk)
|
||||
aircox.default_station = (pk is None)
|
||||
|
||||
|
||||
def init_timezone(self, request, aircox):
|
||||
# note: later we can use http://freegeoip.net/ on user side if
|
||||
# required
|
||||
# TODO: add to request's session
|
||||
timezone = None
|
||||
try:
|
||||
timezone = request.session.get('aircox.timezone')
|
||||
|
@ -81,6 +74,7 @@ class AircoxMiddleware(object):
|
|||
timezone = tz.get_current_timezone()
|
||||
tz.activate(timezone)
|
||||
|
||||
|
||||
def __call__(self, request):
|
||||
tz.activate(pytz.timezone('Europe/Brussels'))
|
||||
aircox = AircoxInfo()
|
||||
|
|
137
aircox/models.py
137
aircox/models.py
|
@ -12,7 +12,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -29,8 +29,8 @@ logger = logging.getLogger('aircox.core')
|
|||
#
|
||||
# Abstracts
|
||||
#
|
||||
class RelatedManager(models.Manager):
|
||||
def get_for(self, object = None, model = None, qs = None):
|
||||
class RelatedQuerySet(models.QuerySet):
|
||||
def related(self, object = None, model = None):
|
||||
"""
|
||||
Return a queryset that filter on the given object or model(s)
|
||||
|
||||
|
@ -40,18 +40,18 @@ class RelatedManager(models.Manager):
|
|||
if not model and object:
|
||||
model = type(object)
|
||||
|
||||
qs = self if qs is None else qs
|
||||
qs = self
|
||||
if hasattr(model, '__iter__'):
|
||||
model = [ ContentType.objects.get_for_model(m).id
|
||||
for m in model ]
|
||||
qs = qs.filter(related_type__pk__in = model)
|
||||
self = self.filter(related_type__pk__in = model)
|
||||
else:
|
||||
model = ContentType.objects.get_for_model(model)
|
||||
qs = qs.filter(related_type__pk = model.id)
|
||||
self = self.filter(related_type__pk = model.id)
|
||||
|
||||
if object:
|
||||
qs = qs.filter(related_id = object.pk)
|
||||
return qs
|
||||
self = self.filter(related_id = object.pk)
|
||||
return self
|
||||
|
||||
class Related(models.Model):
|
||||
"""
|
||||
|
@ -72,7 +72,7 @@ class Related(models.Model):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
objects = RelatedManager()
|
||||
objects = RelatedQuerySet.as_manager()
|
||||
|
||||
@classmethod
|
||||
def ReverseField(cl):
|
||||
|
@ -154,6 +154,21 @@ class Track(Related):
|
|||
#
|
||||
# Station related classes
|
||||
#
|
||||
class StationQuerySet(models.QuerySet):
|
||||
def default(self, station = None):
|
||||
"""
|
||||
Return station model instance, using defaults or
|
||||
given one.
|
||||
"""
|
||||
if station is None:
|
||||
return self.order_by('-default', 'pk').first()
|
||||
return self.filter(pk = station).first()
|
||||
|
||||
def default_station():
|
||||
""" Return default station (used by model fields) """
|
||||
return Station.objects.default()
|
||||
|
||||
|
||||
class Station(Nameable):
|
||||
"""
|
||||
Represents a radio station, to which multiple programs are attached
|
||||
|
@ -175,6 +190,8 @@ class Station(Nameable):
|
|||
help_text = _('if checked, this station is used as the main one')
|
||||
)
|
||||
|
||||
objects = StationQuerySet.as_manager()
|
||||
|
||||
#
|
||||
# Controllers
|
||||
#
|
||||
|
@ -233,12 +250,6 @@ class Station(Nameable):
|
|||
self.__prepare_controls()
|
||||
return self.__streamer
|
||||
|
||||
def raw_on_air(self, **kwargs):
|
||||
"""
|
||||
Forward call to Log.objects.on_air for this station
|
||||
"""
|
||||
return Log.objects.station(self).on_air().filter(**kwargs)
|
||||
|
||||
def on_air(self, date = None, count = 0, no_cache = False):
|
||||
"""
|
||||
Return a queryset of what happened on air, based on logs and
|
||||
|
@ -249,11 +260,10 @@ class Station(Nameable):
|
|||
|
||||
If date is not specified, count MUST be set to a non-zero value.
|
||||
|
||||
It is different from Station.raw_on_air method since it filters
|
||||
It is different from Logs.on_air method since it filters
|
||||
out elements that should have not been on air, such as a stream
|
||||
that has been played when there was a live diffusion.
|
||||
"""
|
||||
# FIXME: as an iterator?
|
||||
# TODO argument to get sound instead of tracks
|
||||
if not date and not count:
|
||||
raise ValueError('at least one argument must be set')
|
||||
|
@ -267,7 +277,7 @@ class Station(Nameable):
|
|||
|
||||
now = tz.now()
|
||||
if date:
|
||||
logs = Log.objects.station(self).at(date)
|
||||
logs = Log.objects.at(date)
|
||||
diffs = Diffusion.objects.station(self).at(date) \
|
||||
.filter(start__lte = now, type = Diffusion.Type.normal) \
|
||||
.order_by('-start')
|
||||
|
@ -280,7 +290,7 @@ class Station(Nameable):
|
|||
|
||||
q = models.Q(diffusion__isnull = False) | \
|
||||
models.Q(track__isnull = False)
|
||||
logs = logs.filter(q, type = Log.Type.on_air).order_by('-date')
|
||||
logs = logs.station(self).on_air().filter(q).order_by('-date')
|
||||
|
||||
# filter out tracks played when there was a diffusion
|
||||
n = 0
|
||||
|
@ -288,7 +298,9 @@ class Station(Nameable):
|
|||
for diff in diffs:
|
||||
if count and n >= count:
|
||||
break
|
||||
q = q | models.Q(date__gte = diff.start, end__lte = diff.end)
|
||||
# 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)
|
||||
n += 1
|
||||
logs = logs.exclude(q, diffusion__isnull = True)
|
||||
|
||||
|
@ -317,6 +329,7 @@ class ProgramManager(models.Manager):
|
|||
qs = self if qs is None else qs
|
||||
return qs.filter(station = station, **kwargs)
|
||||
|
||||
|
||||
class Program(Nameable):
|
||||
"""
|
||||
A Program can either be a Streamed or a Scheduled program.
|
||||
|
@ -461,7 +474,6 @@ class Stream(models.Model):
|
|||
)
|
||||
|
||||
|
||||
|
||||
# BIG FIXME: self.date is still used as datetime
|
||||
class Schedule(models.Model):
|
||||
"""
|
||||
|
@ -502,7 +514,7 @@ class Schedule(models.Model):
|
|||
)
|
||||
timezone = models.CharField(
|
||||
_('timezone'),
|
||||
default = pytz.UTC,
|
||||
default = tz.get_current_timezone,
|
||||
choices = [(x, x) for x in pytz.all_timezones],
|
||||
max_length = 100,
|
||||
help_text = _('timezone used for the date')
|
||||
|
@ -831,10 +843,10 @@ class Diffusion(models.Model):
|
|||
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
|
||||
)
|
||||
initial = models.ForeignKey (
|
||||
'self',
|
||||
verbose_name = _('initial diffusion'),
|
||||
'self', on_delete=models.SET_NULL,
|
||||
blank = True, null = True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name = 'reruns',
|
||||
verbose_name = _('initial diffusion'),
|
||||
help_text = _('the diffusion is a rerun of this one')
|
||||
)
|
||||
# port = models.ForeignKey(
|
||||
|
@ -885,7 +897,15 @@ class Diffusion(models.Model):
|
|||
"""
|
||||
return tz.localtime(self.end, tz.get_current_timezone())
|
||||
|
||||
@property
|
||||
def original(self):
|
||||
""" Return the original diffusion (self or initial) """
|
||||
return self.initial if self.initial else self
|
||||
|
||||
def is_live(self):
|
||||
"""
|
||||
True if Diffusion is live (False if there are sounds files)
|
||||
"""
|
||||
return self.type == self.Type.normal and \
|
||||
not self.get_sounds(archive = True).count()
|
||||
|
||||
|
@ -948,9 +968,8 @@ class Diffusion(models.Model):
|
|||
return super().save(*args, **kwargs)
|
||||
|
||||
if self.initial:
|
||||
# force link to the first diffusion
|
||||
if self.initial.initial:
|
||||
self.initial = self.initial.initial
|
||||
# enforce link to the original diffusion
|
||||
self.initial = self.initial.original
|
||||
self.program = self.initial.program
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
@ -1271,22 +1290,20 @@ class LogQuerySet(models.QuerySet):
|
|||
# models.Q(date__lte = end))
|
||||
return self.filter(date__gte = start, date__lte = end)
|
||||
|
||||
def on_air(self, date = None):
|
||||
"""
|
||||
Return a queryset of the played elements' log for the given
|
||||
station and model. This queryset is ordered by date ascending
|
||||
def on_air(self):
|
||||
return self.filter(type = Log.Type.on_air)
|
||||
|
||||
* station: return logs occuring on this station
|
||||
* date: only return logs that occured at this date
|
||||
* kwargs: extra filter kwargs
|
||||
"""
|
||||
if date:
|
||||
qs = self.at(date)
|
||||
else:
|
||||
qs = self
|
||||
def start(self):
|
||||
return self.filter(type = Log.Type.start)
|
||||
|
||||
qs = qs.filter(type = Log.Type.on_air)
|
||||
return qs.order_by('date')
|
||||
def with_diff(self, with_it = True):
|
||||
return self.filter(diffusion__isnull = not with_it)
|
||||
|
||||
def with_sound(self, with_it = True):
|
||||
return self.filter(sound__isnull = not with_it)
|
||||
|
||||
def with_track(self, with_it = True):
|
||||
return self.filter(track__isnull = not with_it)
|
||||
|
||||
@staticmethod
|
||||
def _get_archive_path(station, date):
|
||||
|
@ -1452,12 +1469,6 @@ class Log(models.Model):
|
|||
default=tz.now,
|
||||
db_index = True,
|
||||
)
|
||||
# date of the next diffusion: used in order to ease on_air algo's
|
||||
end = models.DateTimeField(
|
||||
_('end'),
|
||||
default=tz.now,
|
||||
db_index = True,
|
||||
)
|
||||
comment = models.CharField(
|
||||
_('comment'),
|
||||
max_length = 512,
|
||||
|
@ -1488,16 +1499,6 @@ class Log(models.Model):
|
|||
|
||||
objects = LogQuerySet.as_manager()
|
||||
|
||||
def estimate_end(self):
|
||||
"""
|
||||
Calculated end using self.related informations
|
||||
"""
|
||||
if self.diffusion:
|
||||
return self.diffusion.end
|
||||
if self.sound:
|
||||
return self.date + utils.to_timedelta(self.sound.duration)
|
||||
return self.date
|
||||
|
||||
@property
|
||||
def related(self):
|
||||
return self.diffusion or self.sound or self.track
|
||||
|
@ -1511,21 +1512,6 @@ class Log(models.Model):
|
|||
"""
|
||||
return tz.localtime(self.date, tz.get_current_timezone())
|
||||
|
||||
def is_expired(self, date = None):
|
||||
"""
|
||||
Return True if the log is expired. Note that it only check
|
||||
against the date, so it is still possible that the expiration
|
||||
occured because of a Stop or other source.
|
||||
|
||||
For sound logs, also check against sound duration when
|
||||
end == date (e.g after a crash)
|
||||
"""
|
||||
date = utils.date_or_default(date)
|
||||
end = self.end
|
||||
if end == self.date and self.sound:
|
||||
end = self.date + to_timedelta(self.sound.duration)
|
||||
return end < date
|
||||
|
||||
def print(self):
|
||||
r = []
|
||||
if self.diffusion:
|
||||
|
@ -1549,8 +1535,3 @@ class Log(models.Model):
|
|||
self.local_date.strftime('%Y/%m/%d %H:%M%z'),
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.end:
|
||||
self.end = self.estimate_end()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -1,57 +1,626 @@
|
|||
/**
|
||||
* Define rules for the default layouts, and some useful classes
|
||||
*/
|
||||
|
||||
/** general **/
|
||||
body {
|
||||
background-color: #373737;
|
||||
background-color: #F2F2F2;
|
||||
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em;
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
|
||||
margin: 0.4em 0em;
|
||||
}
|
||||
|
||||
h1:first-letter, h2:first-letter, h3:first-letter, h4:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.4em; }
|
||||
h2 { font-size: 1.2em; }
|
||||
h3 { font-size: 0.9em; }
|
||||
h4 { font-size: 0.8em; }
|
||||
|
||||
h1 > *, h2 > *, h3 > *, h4 > * { vertical-align: middle; }
|
||||
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
a:hover { color: #007EDF; }
|
||||
a:hover > .small_icon { box-shadow: 0em 0em 0.1em #007EDF; }
|
||||
|
||||
ul { margin: 0em; }
|
||||
|
||||
|
||||
/**** position & box ****/
|
||||
.float_right { float: right; }
|
||||
.float_left { float: left; }
|
||||
|
||||
|
||||
.flex_row {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex_row > .flex_item,
|
||||
.flex_column > .flex_item {
|
||||
-webkit-flex: auto;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
.small {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**** indicators & info ****/
|
||||
time, .tags {
|
||||
font-size: 0.9em;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em;
|
||||
color: #007EDF;
|
||||
}
|
||||
|
||||
.error { color: red; }
|
||||
.warning { color: orange; }
|
||||
.success { color: green; }
|
||||
|
||||
.icon {
|
||||
max-width: 2em;
|
||||
max-height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.small_icon {
|
||||
max-height: 1.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/** main layout **/
|
||||
body > * {
|
||||
max-width: 92em;
|
||||
margin: 0em auto;
|
||||
padding: 0em;
|
||||
}
|
||||
|
||||
|
||||
.menu {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.menu:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: #f2f2f2;
|
||||
border: 1px black solid;
|
||||
width: 80%;
|
||||
|
||||
.menu.row section {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.menu.col > section {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
|
||||
/**** top + header layout ****/
|
||||
body > .top {
|
||||
position: fixed;
|
||||
z-index: 10000000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
margin: 0em auto;
|
||||
background-color: white;
|
||||
border-bottom: 0.1em #dfdfdf solid;
|
||||
box-shadow: 0em 0.1em 0.1em rgba(255,255,255,0.7);
|
||||
box-shadow: 0em 0.1em 0.5em rgba(0,0,0,0.1);
|
||||
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
body > .top > .menu {
|
||||
max-width: 92em;
|
||||
height: 2.5em;
|
||||
margin: 0em auto;
|
||||
}
|
||||
|
||||
body[scrollY] > .top {
|
||||
opacity: 0.1;
|
||||
transition: opacity 1.5s 1s;
|
||||
}
|
||||
|
||||
body > .top:hover {
|
||||
opacity: 1.0;
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
|
||||
body > .header {
|
||||
overflow: hidden;
|
||||
margin-top: 3.3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/** FIXME: remove this once image slides impled **/
|
||||
body > .header > div {
|
||||
width: 15000%;
|
||||
}
|
||||
|
||||
body > .header > div > section {
|
||||
margin: 0;
|
||||
margin-right: -0.4em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**** page layout ****/
|
||||
.page {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.page > main {
|
||||
flex: auto;
|
||||
|
||||
overflow: hidden;
|
||||
margin: 0em 0em;
|
||||
border-radius: 0.4em;
|
||||
border: 0.1em #dfdfdf solid;
|
||||
|
||||
background-color: rgba(255,255,255,0.9);
|
||||
box-shadow: inset 0.1em 0.1em 0.2em rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
|
||||
.page > nav {
|
||||
flex: 1;
|
||||
width: 50em;
|
||||
overflow: hidden;
|
||||
max-width: 16em;
|
||||
}
|
||||
|
||||
.page > .menu.col:first-child { margin-right: 2em; }
|
||||
.page > main + .menu.col { margin-left: 2em; }
|
||||
|
||||
|
||||
|
||||
/**** page main ****/
|
||||
main:not(.detail) h1 {
|
||||
margin: 0em 0em 0.4em 0em;
|
||||
}
|
||||
|
||||
main .post_content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
main .post_content section {
|
||||
display: inline-block;
|
||||
width: calc(50% - 1em);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
main.detail {
|
||||
padding: 0em;
|
||||
margin: 0em;
|
||||
}
|
||||
|
||||
main > .content {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
main > header {
|
||||
margin: 0em;
|
||||
padding: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
main > header .foreground {
|
||||
position: absolute;
|
||||
left: 0em;
|
||||
top: 0em;
|
||||
width: calc(100% - 2em);
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
main > header h1 {
|
||||
width: calc(100% - 2em);
|
||||
margin: 0em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
main header .headline {
|
||||
display: inline-block;
|
||||
width: calc(60% - 0.8em);
|
||||
min-height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
main > header .background {
|
||||
margin: -1em;
|
||||
height: 17em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
main > header .background img {
|
||||
position: absolute;
|
||||
/*! top: -40%; */
|
||||
/*! left: -40%; */
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
filter: blur(20px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
main > header .cover {
|
||||
right: 0em;
|
||||
top: 1em;
|
||||
width: auto;
|
||||
max-height: calc(100% - 2em);
|
||||
max-width: calc(40% - 2em);
|
||||
margin: 1em;
|
||||
position: absolute;
|
||||
box-shadow: 0em 0em 4em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** sections **/
|
||||
body section ul {
|
||||
padding: 0em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
/**** link list ****/
|
||||
.menu.row .section_link_list > a {
|
||||
display: inline-block;
|
||||
margin: 0.2em 1em;
|
||||
}
|
||||
|
||||
.menu.col .section_link_list > a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** content: menus **/
|
||||
/** content: list & items **/
|
||||
.list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul.list, .list > ul {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.list_item {
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.list_item > *:not(:last-child) {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.list_item img.cover.big {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
min-height: 15em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td {
|
||||
margin: 0;
|
||||
padding: 0 0.4em;
|
||||
.list_item img.cover.small {
|
||||
margin-right: 0.4em;
|
||||
border-radius: 0.4em;
|
||||
float: left;
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 0.4em;
|
||||
.list_item > * {
|
||||
margin: 0em 0.2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr:not(.header):hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
tr.header {
|
||||
background-color: #212121;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
tr.bottom > td {
|
||||
vertical-align: top;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
tr.subdata {
|
||||
font-style: italic;
|
||||
.list nav {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
/** content: list items in full page **/
|
||||
.content > .list:not(.date_list) .list_item {
|
||||
min-width: 20em;
|
||||
display: inline-block;
|
||||
min-height: 2.5em;
|
||||
margin: 0.4em;
|
||||
}
|
||||
|
||||
/** content: date list **/
|
||||
.date_list nav {
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.date_list nav a {
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.date_list nav a.date {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.date_list nav a[selected] {
|
||||
color: #007EDF;
|
||||
border-bottom: 0.2em #007EDF dotted;
|
||||
}
|
||||
|
||||
.date_list ul:not([selected]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.date_list ul:target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date_list h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.date_list_item .cover.small {
|
||||
width: 64px;
|
||||
margin: 0.4em;
|
||||
}
|
||||
|
||||
.date_list_item h3 {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
.date_list_item time {
|
||||
color: #007EDF;
|
||||
}
|
||||
|
||||
|
||||
.date_list_item.now {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.date_list_item img.now {
|
||||
width: 1.3em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/** content: date list in full page **/
|
||||
.content > .date_list .date_list_item time {
|
||||
color: #007EDF;
|
||||
font-size: 1.1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.content > .date_list .date_list_item:nth-child(2n+1),
|
||||
.date_list_item.now {
|
||||
box-shadow: inset 0em 0em 3em rgba(0, 124, 226, 0.1);
|
||||
background-color: rgba(0, 124, 226, 0.05);
|
||||
}
|
||||
|
||||
.content > .date_list {
|
||||
padding: 0 10%;
|
||||
margin: auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
|
||||
/** content: comments **/
|
||||
.comments form input:not([type=checkbox]),
|
||||
.comments form textarea {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-height: 6em;
|
||||
margin: 0.2em 0em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.comments form input[type=checkbox],
|
||||
.comments form button[type=submit] {
|
||||
vertical-align:bottom;
|
||||
margin: 0.2em 0em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comments form button[type=submit] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.comments form #show_more:not(:checked) ~ .extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comments label[for="show_more"] {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.comments ul {
|
||||
margin-top: 2.5em;
|
||||
}
|
||||
|
||||
.comment {
|
||||
list-style: none;
|
||||
border: 1px #818181 dotted;
|
||||
margin: 0.4em 0em;
|
||||
}
|
||||
|
||||
.comment .metadata {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.comment time {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/** component: sound **/
|
||||
.component.sound {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0.2em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.component.sound[state="play"] button {
|
||||
animation-name: sound-blink;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@keyframes sound-blink {
|
||||
from { background-color: rgba(255, 255, 255, 0); }
|
||||
to { background-color: rgba(255, 255, 255, 0.6); }
|
||||
}
|
||||
|
||||
|
||||
.component.sound .button {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.component.sound .button > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.component.sound button {
|
||||
transition: background-color 0.5s;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.component.sound button:hover {
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.component.sound button > img {
|
||||
background-color: rgba(255,255,255,0.9);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.component.sound .content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.component.sound .info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.component.sound progress {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 0.4em;
|
||||
}
|
||||
|
||||
.component.sound progress:hover {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
|
||||
/** component: playlist **/
|
||||
.component.playlist footer {
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.component.playlist .read_all {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.component.playlist .read_all + label {
|
||||
display: inline-block;
|
||||
padding: 0.1em;
|
||||
margin-left: 0.2em;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
box-shadow: inset 0em 0em 0.1em #818181;
|
||||
}
|
||||
|
||||
.component.playlist .read_all:not(:checked) + label {
|
||||
border-left: 0.1em #818181 solid;
|
||||
margin-right: 0em;
|
||||
}
|
||||
|
||||
.component.playlist .read_all:checked + label {
|
||||
border-right: 0.1em #007EDF solid;
|
||||
box-shadow: inset 0em 0em 0.1em #007EDF;
|
||||
margin-right: 0em;
|
||||
}
|
||||
|
||||
|
||||
/** content: page **/
|
||||
main .body ~ section:not(.comments) {
|
||||
width: calc(50% - 1em);
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.meta .author .headline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.meta .link_list > a {
|
||||
font-size: 0.9em;
|
||||
margin: 0em 0.1em;
|
||||
padding: 0.2em;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.meta .link_list > a:hover {
|
||||
border-radius: 0.2em;
|
||||
background-color: rgba(0, 126, 223, 0.1);
|
||||
}
|
||||
|
||||
|
||||
/** content: others **/
|
||||
.list_item.track .title {
|
||||
display: inline;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ Monitor.update(50000);
|
|||
<tr>
|
||||
<th class="name" colspan=2>{{ station.name }}</th>
|
||||
<td>
|
||||
{% with station.streamer.current_source.name as current_source %}
|
||||
{% with station.streamer.source.name as current_source %}
|
||||
{% blocktrans %}
|
||||
Current source: {{ current_source }}
|
||||
{% endblocktrans %}
|
||||
|
@ -154,7 +154,7 @@ Monitor.update(50000);
|
|||
{% endif %}
|
||||
</td>
|
||||
<td class="source_info">
|
||||
{% if source.name == station.streamer.current_source.name %}
|
||||
{% if source.name == station.streamer.source.name %}
|
||||
<img src="{% static "aircox/images/play.png" %}" alt="{% trans "current" %}">
|
||||
{% endif %}
|
||||
{% if source.is_dealer %}
|
||||
|
@ -167,7 +167,7 @@ Monitor.update(50000);
|
|||
{% if source.is_dealer %}
|
||||
{{ source.playlist|join:"<br>" }}
|
||||
{% else %}
|
||||
{{ source.current_sound }}
|
||||
{{ source.sound }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
|
|
|
@ -127,7 +127,7 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
|
|||
return Http404
|
||||
|
||||
station.streamer.fetch()
|
||||
source = source or station.streamer.current_source
|
||||
source = source or station.streamer.source
|
||||
if action == 'skip':
|
||||
self.actionSkip(request, station, source)
|
||||
if action == 'restart':
|
||||
|
@ -202,9 +202,8 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
|
|||
stats = self.Stats(station = station, date = date,
|
||||
items = [], tags = {})
|
||||
|
||||
qs = station.raw_on_air(date = date) \
|
||||
.prefetch_related('diffusion', 'sound', 'track',
|
||||
'track__tags')
|
||||
qs = Log.objects.station(station).on_air() \
|
||||
.prefetch_related('diffusion', 'sound', 'track', 'track__tags')
|
||||
if not qs.exists():
|
||||
qs = models.Log.objects.load_archive(station, date)
|
||||
|
||||
|
@ -219,7 +218,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
|
|||
name = rel.program.name,
|
||||
type = _('Diffusion'),
|
||||
col = 0,
|
||||
tracks = models.Track.objects.get_for(object = rel)
|
||||
tracks = models.Track.objects.related(object = rel)
|
||||
.prefetch_related('tags'),
|
||||
)
|
||||
sound_log = None
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if diffusion.diffusion_set.count %}
|
||||
{% if diffusion.reruns.count %}
|
||||
<section class="dates">
|
||||
<h2>{% trans "Dates of diffusion" %}</h2>
|
||||
<ul>
|
||||
{% with diffusion=page.diffusion %}
|
||||
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
|
||||
{% for diffusion in diffusion.diffusion_set.all %}
|
||||
{% for diffusion in diffusion.reruns.all %}
|
||||
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
|
Loading…
Reference in New Issue
Block a user