forked from rc/aircox
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
|
extra = 0
|
||||||
fields = ('artist', 'title', 'info', 'position', 'in_seconds', 'tags')
|
fields = ('artist', 'title', 'info', 'position', 'in_seconds', 'tags')
|
||||||
|
|
||||||
|
list_display = ['artist','title','tags','related']
|
||||||
|
list_filter = ['artist','title','tags']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Sound)
|
@admin.register(Sound)
|
||||||
class SoundAdmin(NameableAdmin):
|
class SoundAdmin(NameableAdmin):
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import os
|
import atexit, logging, os, re, signal, subprocess
|
||||||
import signal
|
|
||||||
import re
|
import tzlocal
|
||||||
import subprocess
|
|
||||||
import atexit
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
import aircox.models as models
|
import aircox.models as models
|
||||||
import aircox.settings as settings
|
import aircox.settings as settings
|
||||||
|
|
||||||
from aircox.connector import Connector
|
from aircox.connector import Connector
|
||||||
|
|
||||||
|
|
||||||
|
local_tz = tzlocal.get_localzone()
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.tools')
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,19 +32,14 @@ class Streamer:
|
||||||
"""
|
"""
|
||||||
Path of the configuration file.
|
Path of the configuration file.
|
||||||
"""
|
"""
|
||||||
current_sound = ''
|
source = None
|
||||||
"""
|
"""
|
||||||
Current sound being played (retrieved by fetch)
|
Current source object that is responsible of self.sound
|
||||||
"""
|
|
||||||
current_source = None
|
|
||||||
"""
|
|
||||||
Current source object that is responsible of self.current_sound
|
|
||||||
"""
|
"""
|
||||||
process = None
|
process = None
|
||||||
"""
|
"""
|
||||||
Application's process if ran from Streamer
|
Application's process if ran from Streamer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
socket_path = ''
|
socket_path = ''
|
||||||
"""
|
"""
|
||||||
Path to the connector's socket
|
Path to the connector's socket
|
||||||
|
@ -95,13 +90,11 @@ class Streamer:
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.current_sound = data.get('initial_uri')
|
self.source = next(
|
||||||
self.current_source = next(
|
|
||||||
iter(source for source in self.station.sources
|
iter(source for source in self.station.sources
|
||||||
if source.rid == rid),
|
if source.rid == rid),
|
||||||
self.current_source
|
self.source
|
||||||
)
|
)
|
||||||
self.current_source.metadata = data
|
|
||||||
|
|
||||||
def push(self, config = True):
|
def push(self, config = True):
|
||||||
"""
|
"""
|
||||||
|
@ -202,43 +195,25 @@ class Source:
|
||||||
Controller of a Source. Value are usually updated directly on the
|
Controller of a Source. Value are usually updated directly on the
|
||||||
external side.
|
external side.
|
||||||
"""
|
"""
|
||||||
program = None
|
station = 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
|
|
||||||
"""
|
|
||||||
connector = None
|
connector = None
|
||||||
"""
|
""" Connector to Liquidsoap server """
|
||||||
Connector to Liquidsoap server
|
program = None
|
||||||
"""
|
""" Related program """
|
||||||
metadata = None
|
name = ''
|
||||||
"""
|
""" Name of the source """
|
||||||
Dict of file's metadata given by Liquidsoap. Set by Stream when
|
path = ''
|
||||||
fetch()ing
|
""" 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
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
|
@ -267,12 +242,6 @@ class Source:
|
||||||
if not self.__playlist:
|
if not self.__playlist:
|
||||||
self.from_db()
|
self.from_db()
|
||||||
|
|
||||||
def is_stream(self):
|
|
||||||
return self.program and not self.program.show
|
|
||||||
|
|
||||||
def is_dealer(self):
|
|
||||||
return not self.program
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def playlist(self):
|
def playlist(self):
|
||||||
"""
|
"""
|
||||||
|
@ -325,11 +294,19 @@ class Source:
|
||||||
if self.__playlist else []
|
if self.__playlist else []
|
||||||
|
|
||||||
#
|
#
|
||||||
# RPC
|
# RPC & States
|
||||||
#
|
#
|
||||||
def _send(self, *args, **kwargs):
|
def _send(self, *args, **kwargs):
|
||||||
return self.connector.send(*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
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
return self._send('var.get ', self.id, '_active') == 'true'
|
return self._send('var.get ', self.id, '_active') == 'true'
|
||||||
|
@ -348,9 +325,15 @@ class Source:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.rid = data.get('rid')
|
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):
|
def push(self):
|
||||||
"""
|
"""
|
||||||
|
@ -383,8 +366,9 @@ class Source:
|
||||||
|
|
||||||
def stream(self):
|
def stream(self):
|
||||||
"""
|
"""
|
||||||
Return a dict with stream info for a Stream program, or None if there
|
Return dict of info for the current Stream program running on
|
||||||
is not. Used in the template.
|
the source. If not, return None.
|
||||||
|
[ used in the templates ]
|
||||||
"""
|
"""
|
||||||
# TODO: multiple streams
|
# TODO: multiple streams
|
||||||
stream = self.program.stream_set.all().first()
|
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 import timezone as tz
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.db import models
|
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
|
# force using UTC
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -61,17 +62,20 @@ class Monitor:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_last_log(self, *args, **kwargs):
|
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) \
|
return Log.objects.station(self.station) \
|
||||||
.filter(*args, **kwargs) \
|
|
||||||
.select_related('diffusion', 'sound') \
|
.select_related('diffusion', 'sound') \
|
||||||
.order_by('pk').last()
|
.order_by('pk')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_log(self):
|
def last_log(self):
|
||||||
"""
|
"""
|
||||||
Last log of monitored station
|
Last log of monitored station
|
||||||
"""
|
"""
|
||||||
return self.get_last_log()
|
return self.log_qs.last()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_sound(self):
|
def last_sound(self):
|
||||||
|
@ -104,7 +108,15 @@ class Monitor:
|
||||||
if not self.streamer.ready():
|
if not self.streamer.ready():
|
||||||
return
|
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.sync_playlists()
|
||||||
self.handle()
|
self.handle()
|
||||||
|
|
||||||
|
@ -116,92 +128,57 @@ class Monitor:
|
||||||
**kwargs)
|
**kwargs)
|
||||||
log.save()
|
log.save()
|
||||||
log.print()
|
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
|
return log
|
||||||
|
|
||||||
def trace(self):
|
def trace_sound(self, source):
|
||||||
"""
|
"""
|
||||||
Check the current_sound of the station and update logs if
|
Return log for current on_air (create and save it if required).
|
||||||
needed.
|
|
||||||
"""
|
"""
|
||||||
self.streamer.fetch()
|
sound_path = source.sound
|
||||||
current_sound = self.streamer.current_sound
|
air_time = source.air_time
|
||||||
current_source = self.streamer.current_source
|
|
||||||
if not current_sound or not current_source:
|
|
||||||
print('no source / no sound', current_sound, current_source)
|
|
||||||
return
|
|
||||||
|
|
||||||
log = self.get_last_log(
|
# check if there is yet a log for this sound on the source
|
||||||
models.Q(sound__isnull = False) |
|
delta = tz.timedelta(seconds=5)
|
||||||
models.Q(diffusion__isnull = False),
|
air_times = (air_time - delta, air_time + delta)
|
||||||
type = Log.Type.on_air
|
|
||||||
|
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.
|
def trace_tracks(self, log):
|
||||||
# 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):
|
|
||||||
"""
|
"""
|
||||||
Log tracks for the given sound log (for streamed programs only).
|
Log tracks for the given sound log (for streamed programs only).
|
||||||
Called by self.trace
|
|
||||||
"""
|
"""
|
||||||
if log.diffusion:
|
if log.diffusion:
|
||||||
return
|
return
|
||||||
|
|
||||||
tracks = Track.objects.get_for(object = log.sound) \
|
tracks = Track.objects.related(object = log.sound) \
|
||||||
.filter(in_seconds = True)
|
.filter(in_seconds = True)
|
||||||
if not tracks.exists():
|
if not tracks.exists():
|
||||||
return
|
return
|
||||||
|
@ -249,7 +226,7 @@ class Monitor:
|
||||||
type = Diffusion.Type.normal,
|
type = Diffusion.Type.normal,
|
||||||
sound__type = Sound.Type.archive,
|
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)
|
date = tz.now() - datetime.timedelta(seconds = self.cancel_timeout)
|
||||||
for diff in qs:
|
for diff in qs:
|
||||||
|
@ -274,17 +251,17 @@ class Monitor:
|
||||||
station = self.station
|
station = self.station
|
||||||
now = tz.now()
|
now = tz.now()
|
||||||
|
|
||||||
log = station.raw_on_air(diffusion__isnull = False) \
|
log = Log.objects.station(station).on_air().with_diff() \
|
||||||
.select_related('diffusion') \
|
.select_related('diffusion') \
|
||||||
.order_by('date').last()
|
.order_by('date').last()
|
||||||
if not log or not log.diffusion.is_date_in_range(now):
|
if not log or not log.diffusion.is_date_in_range(now):
|
||||||
# not running anymore
|
# not running anymore
|
||||||
return None, []
|
return None, []
|
||||||
|
|
||||||
# last sound source change: end of file reached or forced to stop
|
# last sound source change: end of file reached or forced to stop
|
||||||
sounds = station.raw_on_air(sound__isnull = False) \
|
sounds = Log.objects.station(station).on_air().with_sound() \
|
||||||
.filter(date__gte = log.date) \
|
.filter(date__gte = log.date) \
|
||||||
.order_by('date')
|
.order_by('date')
|
||||||
|
|
||||||
if sounds.count() and sounds.last().source != log.source:
|
if sounds.count() and sounds.last().source != log.source:
|
||||||
return None, []
|
return None, []
|
||||||
|
@ -294,7 +271,7 @@ class Monitor:
|
||||||
.filter(source = log.source, pk__gt = log.pk) \
|
.filter(source = log.source, pk__gt = log.pk) \
|
||||||
.exclude(sound__type = Sound.Type.removed)
|
.exclude(sound__type = Sound.Type.removed)
|
||||||
|
|
||||||
remaining = log.diffusion.sounds(archive = True) \
|
remaining = log.diffusion.get_sounds(archive = True) \
|
||||||
.exclude(pk__in = sounds) \
|
.exclude(pk__in = sounds) \
|
||||||
.values_list('path', flat = True)
|
.values_list('path', flat = True)
|
||||||
return log.diffusion, list(remaining)
|
return log.diffusion, list(remaining)
|
||||||
|
|
|
@ -28,17 +28,15 @@ class AircoxMiddleware(object):
|
||||||
This middleware must be set after the middleware
|
This middleware must be set after the middleware
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"""
|
"""
|
||||||
default_qs = models.Station.objects.filter(default = True)
|
|
||||||
|
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = 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')
|
station = request.GET.get('aircox.station')
|
||||||
pk = None
|
pk = None
|
||||||
try:
|
try:
|
||||||
if station:
|
if station is not None:
|
||||||
pk = request.GET['aircox.station']
|
pk = request.GET['aircox.station']
|
||||||
if station:
|
if station:
|
||||||
pk = int(pk)
|
pk = int(pk)
|
||||||
|
@ -47,28 +45,23 @@ class AircoxMiddleware(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# select current station
|
def init_station(self, request, aircox):
|
||||||
station = None
|
self.update_station(request)
|
||||||
pk = None
|
|
||||||
try:
|
try:
|
||||||
pk = request.session.get('aircox.station')
|
pk = request.session.get('aircox.station')
|
||||||
if pk:
|
pk = int(pk) if pk else None
|
||||||
pk = int(pk)
|
|
||||||
station = models.Station.objects.filter(pk = pk).first()
|
|
||||||
except:
|
except:
|
||||||
pass
|
|
||||||
|
|
||||||
if not station:
|
|
||||||
pk = None
|
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)
|
aircox.default_station = (pk is None)
|
||||||
|
|
||||||
|
|
||||||
def init_timezone(self, request, aircox):
|
def init_timezone(self, request, aircox):
|
||||||
# note: later we can use http://freegeoip.net/ on user side if
|
# note: later we can use http://freegeoip.net/ on user side if
|
||||||
# required
|
# required
|
||||||
|
# TODO: add to request's session
|
||||||
timezone = None
|
timezone = None
|
||||||
try:
|
try:
|
||||||
timezone = request.session.get('aircox.timezone')
|
timezone = request.session.get('aircox.timezone')
|
||||||
|
@ -81,6 +74,7 @@ class AircoxMiddleware(object):
|
||||||
timezone = tz.get_current_timezone()
|
timezone = tz.get_current_timezone()
|
||||||
tz.activate(timezone)
|
tz.activate(timezone)
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
tz.activate(pytz.timezone('Europe/Brussels'))
|
tz.activate(pytz.timezone('Europe/Brussels'))
|
||||||
aircox = AircoxInfo()
|
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.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
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 import timezone as tz
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -29,8 +29,8 @@ logger = logging.getLogger('aircox.core')
|
||||||
#
|
#
|
||||||
# Abstracts
|
# Abstracts
|
||||||
#
|
#
|
||||||
class RelatedManager(models.Manager):
|
class RelatedQuerySet(models.QuerySet):
|
||||||
def get_for(self, object = None, model = None, qs = None):
|
def related(self, object = None, model = None):
|
||||||
"""
|
"""
|
||||||
Return a queryset that filter on the given object or model(s)
|
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:
|
if not model and object:
|
||||||
model = type(object)
|
model = type(object)
|
||||||
|
|
||||||
qs = self if qs is None else qs
|
qs = self
|
||||||
if hasattr(model, '__iter__'):
|
if hasattr(model, '__iter__'):
|
||||||
model = [ ContentType.objects.get_for_model(m).id
|
model = [ ContentType.objects.get_for_model(m).id
|
||||||
for m in model ]
|
for m in model ]
|
||||||
qs = qs.filter(related_type__pk__in = model)
|
self = self.filter(related_type__pk__in = model)
|
||||||
else:
|
else:
|
||||||
model = ContentType.objects.get_for_model(model)
|
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:
|
if object:
|
||||||
qs = qs.filter(related_id = object.pk)
|
self = self.filter(related_id = object.pk)
|
||||||
return qs
|
return self
|
||||||
|
|
||||||
class Related(models.Model):
|
class Related(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -72,7 +72,7 @@ class Related(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
objects = RelatedManager()
|
objects = RelatedQuerySet.as_manager()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ReverseField(cl):
|
def ReverseField(cl):
|
||||||
|
@ -154,6 +154,21 @@ class Track(Related):
|
||||||
#
|
#
|
||||||
# Station related classes
|
# 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):
|
class Station(Nameable):
|
||||||
"""
|
"""
|
||||||
Represents a radio station, to which multiple programs are attached
|
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')
|
help_text = _('if checked, this station is used as the main one')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = StationQuerySet.as_manager()
|
||||||
|
|
||||||
#
|
#
|
||||||
# Controllers
|
# Controllers
|
||||||
#
|
#
|
||||||
|
@ -233,12 +250,6 @@ class Station(Nameable):
|
||||||
self.__prepare_controls()
|
self.__prepare_controls()
|
||||||
return self.__streamer
|
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):
|
def on_air(self, date = None, count = 0, no_cache = False):
|
||||||
"""
|
"""
|
||||||
Return a queryset of what happened on air, based on logs and
|
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.
|
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
|
out elements that should have not been on air, such as a stream
|
||||||
that has been played when there was a live diffusion.
|
that has been played when there was a live diffusion.
|
||||||
"""
|
"""
|
||||||
# FIXME: as an iterator?
|
|
||||||
# TODO argument to get sound instead of tracks
|
# TODO argument to get sound instead of tracks
|
||||||
if not date and not count:
|
if not date and not count:
|
||||||
raise ValueError('at least one argument must be set')
|
raise ValueError('at least one argument must be set')
|
||||||
|
@ -267,7 +277,7 @@ class Station(Nameable):
|
||||||
|
|
||||||
now = tz.now()
|
now = tz.now()
|
||||||
if date:
|
if date:
|
||||||
logs = Log.objects.station(self).at(date)
|
logs = Log.objects.at(date)
|
||||||
diffs = Diffusion.objects.station(self).at(date) \
|
diffs = Diffusion.objects.station(self).at(date) \
|
||||||
.filter(start__lte = now, type = Diffusion.Type.normal) \
|
.filter(start__lte = now, type = Diffusion.Type.normal) \
|
||||||
.order_by('-start')
|
.order_by('-start')
|
||||||
|
@ -280,7 +290,7 @@ class Station(Nameable):
|
||||||
|
|
||||||
q = models.Q(diffusion__isnull = False) | \
|
q = models.Q(diffusion__isnull = False) | \
|
||||||
models.Q(track__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
|
# filter out tracks played when there was a diffusion
|
||||||
n = 0
|
n = 0
|
||||||
|
@ -288,7 +298,9 @@ class Station(Nameable):
|
||||||
for diff in diffs:
|
for diff in diffs:
|
||||||
if count and n >= count:
|
if count and n >= count:
|
||||||
break
|
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
|
n += 1
|
||||||
logs = logs.exclude(q, diffusion__isnull = True)
|
logs = logs.exclude(q, diffusion__isnull = True)
|
||||||
|
|
||||||
|
@ -317,6 +329,7 @@ class ProgramManager(models.Manager):
|
||||||
qs = self if qs is None else qs
|
qs = self if qs is None else qs
|
||||||
return qs.filter(station = station, **kwargs)
|
return qs.filter(station = station, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Program(Nameable):
|
class Program(Nameable):
|
||||||
"""
|
"""
|
||||||
A Program can either be a Streamed or a Scheduled program.
|
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
|
# BIG FIXME: self.date is still used as datetime
|
||||||
class Schedule(models.Model):
|
class Schedule(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -502,7 +514,7 @@ class Schedule(models.Model):
|
||||||
)
|
)
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
_('timezone'),
|
_('timezone'),
|
||||||
default = pytz.UTC,
|
default = tz.get_current_timezone,
|
||||||
choices = [(x, x) for x in pytz.all_timezones],
|
choices = [(x, x) for x in pytz.all_timezones],
|
||||||
max_length = 100,
|
max_length = 100,
|
||||||
help_text = _('timezone used for the date')
|
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() ],
|
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
|
||||||
)
|
)
|
||||||
initial = models.ForeignKey (
|
initial = models.ForeignKey (
|
||||||
'self',
|
'self', on_delete=models.SET_NULL,
|
||||||
verbose_name = _('initial diffusion'),
|
|
||||||
blank = True, null = True,
|
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')
|
help_text = _('the diffusion is a rerun of this one')
|
||||||
)
|
)
|
||||||
# port = models.ForeignKey(
|
# port = models.ForeignKey(
|
||||||
|
@ -885,7 +897,15 @@ class Diffusion(models.Model):
|
||||||
"""
|
"""
|
||||||
return tz.localtime(self.end, tz.get_current_timezone())
|
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):
|
def is_live(self):
|
||||||
|
"""
|
||||||
|
True if Diffusion is live (False if there are sounds files)
|
||||||
|
"""
|
||||||
return self.type == self.Type.normal and \
|
return self.type == self.Type.normal and \
|
||||||
not self.get_sounds(archive = True).count()
|
not self.get_sounds(archive = True).count()
|
||||||
|
|
||||||
|
@ -948,9 +968,8 @@ class Diffusion(models.Model):
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.initial:
|
if self.initial:
|
||||||
# force link to the first diffusion
|
# enforce link to the original diffusion
|
||||||
if self.initial.initial:
|
self.initial = self.initial.original
|
||||||
self.initial = self.initial.initial
|
|
||||||
self.program = self.initial.program
|
self.program = self.initial.program
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
@ -1271,22 +1290,20 @@ class LogQuerySet(models.QuerySet):
|
||||||
# models.Q(date__lte = end))
|
# models.Q(date__lte = end))
|
||||||
return self.filter(date__gte = start, date__lte = end)
|
return self.filter(date__gte = start, date__lte = end)
|
||||||
|
|
||||||
def on_air(self, date = None):
|
def on_air(self):
|
||||||
"""
|
return self.filter(type = Log.Type.on_air)
|
||||||
Return a queryset of the played elements' log for the given
|
|
||||||
station and model. This queryset is ordered by date ascending
|
|
||||||
|
|
||||||
* station: return logs occuring on this station
|
def start(self):
|
||||||
* date: only return logs that occured at this date
|
return self.filter(type = Log.Type.start)
|
||||||
* kwargs: extra filter kwargs
|
|
||||||
"""
|
|
||||||
if date:
|
|
||||||
qs = self.at(date)
|
|
||||||
else:
|
|
||||||
qs = self
|
|
||||||
|
|
||||||
qs = qs.filter(type = Log.Type.on_air)
|
def with_diff(self, with_it = True):
|
||||||
return qs.order_by('date')
|
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
|
@staticmethod
|
||||||
def _get_archive_path(station, date):
|
def _get_archive_path(station, date):
|
||||||
|
@ -1452,12 +1469,6 @@ class Log(models.Model):
|
||||||
default=tz.now,
|
default=tz.now,
|
||||||
db_index = True,
|
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 = models.CharField(
|
||||||
_('comment'),
|
_('comment'),
|
||||||
max_length = 512,
|
max_length = 512,
|
||||||
|
@ -1488,16 +1499,6 @@ class Log(models.Model):
|
||||||
|
|
||||||
objects = LogQuerySet.as_manager()
|
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
|
@property
|
||||||
def related(self):
|
def related(self):
|
||||||
return self.diffusion or self.sound or self.track
|
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())
|
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):
|
def print(self):
|
||||||
r = []
|
r = []
|
||||||
if self.diffusion:
|
if self.diffusion:
|
||||||
|
@ -1549,8 +1535,3 @@ class Log(models.Model):
|
||||||
self.local_date.strftime('%Y/%m/%d %H:%M%z'),
|
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 {
|
body {
|
||||||
background-color: #373737;
|
|
||||||
background-color: #F2F2F2;
|
background-color: #F2F2F2;
|
||||||
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
|
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
h1, h2, h3, h4, h5 {
|
||||||
padding: 1em;
|
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;
|
padding: 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
|
||||||
background-color: #f2f2f2;
|
.menu.row section {
|
||||||
border: 1px black solid;
|
display: inline-block;
|
||||||
width: 80%;
|
}
|
||||||
|
|
||||||
|
.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;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
.list_item img.cover.small {
|
||||||
margin: 0;
|
margin-right: 0.4em;
|
||||||
padding: 0 0.4em;
|
border-radius: 0.4em;
|
||||||
|
float: left;
|
||||||
|
min-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
.list_item > * {
|
||||||
text-align: left;
|
margin: 0em 0.2em;
|
||||||
font-weight: normal;
|
vertical-align: middle;
|
||||||
margin: 0;
|
|
||||||
padding: 0.4em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:not(.header):hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.header {
|
.list nav {
|
||||||
background-color: #212121;
|
text-align: center;
|
||||||
color: #eee;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.bottom > td {
|
|
||||||
vertical-align: top;
|
/** content: list items in full page **/
|
||||||
padding: 0.4em;
|
.content > .list:not(.date_list) .list_item {
|
||||||
}
|
min-width: 20em;
|
||||||
|
display: inline-block;
|
||||||
tr.subdata {
|
min-height: 2.5em;
|
||||||
font-style: italic;
|
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;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,7 @@ Monitor.update(50000);
|
||||||
<tr>
|
<tr>
|
||||||
<th class="name" colspan=2>{{ station.name }}</th>
|
<th class="name" colspan=2>{{ station.name }}</th>
|
||||||
<td>
|
<td>
|
||||||
{% with station.streamer.current_source.name as current_source %}
|
{% with station.streamer.source.name as current_source %}
|
||||||
{% blocktrans %}
|
{% blocktrans %}
|
||||||
Current source: {{ current_source }}
|
Current source: {{ current_source }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
@ -154,7 +154,7 @@ Monitor.update(50000);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="source_info">
|
<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" %}">
|
<img src="{% static "aircox/images/play.png" %}" alt="{% trans "current" %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if source.is_dealer %}
|
{% if source.is_dealer %}
|
||||||
|
@ -167,7 +167,7 @@ Monitor.update(50000);
|
||||||
{% if source.is_dealer %}
|
{% if source.is_dealer %}
|
||||||
{{ source.playlist|join:"<br>" }}
|
{{ source.playlist|join:"<br>" }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ source.current_sound }}
|
{{ source.sound }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
|
|
|
@ -127,7 +127,7 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
|
||||||
return Http404
|
return Http404
|
||||||
|
|
||||||
station.streamer.fetch()
|
station.streamer.fetch()
|
||||||
source = source or station.streamer.current_source
|
source = source or station.streamer.source
|
||||||
if action == 'skip':
|
if action == 'skip':
|
||||||
self.actionSkip(request, station, source)
|
self.actionSkip(request, station, source)
|
||||||
if action == 'restart':
|
if action == 'restart':
|
||||||
|
@ -202,9 +202,8 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
|
||||||
stats = self.Stats(station = station, date = date,
|
stats = self.Stats(station = station, date = date,
|
||||||
items = [], tags = {})
|
items = [], tags = {})
|
||||||
|
|
||||||
qs = station.raw_on_air(date = date) \
|
qs = Log.objects.station(station).on_air() \
|
||||||
.prefetch_related('diffusion', 'sound', 'track',
|
.prefetch_related('diffusion', 'sound', 'track', 'track__tags')
|
||||||
'track__tags')
|
|
||||||
if not qs.exists():
|
if not qs.exists():
|
||||||
qs = models.Log.objects.load_archive(station, date)
|
qs = models.Log.objects.load_archive(station, date)
|
||||||
|
|
||||||
|
@ -219,7 +218,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
|
||||||
name = rel.program.name,
|
name = rel.program.name,
|
||||||
type = _('Diffusion'),
|
type = _('Diffusion'),
|
||||||
col = 0,
|
col = 0,
|
||||||
tracks = models.Track.objects.get_for(object = rel)
|
tracks = models.Track.objects.related(object = rel)
|
||||||
.prefetch_related('tags'),
|
.prefetch_related('tags'),
|
||||||
)
|
)
|
||||||
sound_log = None
|
sound_log = None
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if diffusion.diffusion_set.count %}
|
{% if diffusion.reruns.count %}
|
||||||
<section class="dates">
|
<section class="dates">
|
||||||
<h2>{% trans "Dates of diffusion" %}</h2>
|
<h2>{% trans "Dates of diffusion" %}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% with diffusion=page.diffusion %}
|
{% with diffusion=page.diffusion %}
|
||||||
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
|
<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>
|
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user