From c059a33077c0af0e28b61b3b5f84a729050e11a7 Mon Sep 17 00:00:00 2001 From: bkfox Date: Wed, 18 Jul 2018 02:22:29 +0200 Subject: [PATCH] fix bug in streamer; clean-up; wider sidebar --- aircox/admin.py | 3 + aircox/controllers.py | 112 ++- aircox/management/commands/streamer.py | 147 ++-- aircox/middleware.py | 28 +- aircox/models.py | 137 ++-- aircox/static/aircox/css/layout.css | 637 +++++++++++++++++- .../templates/aircox/controllers/monitor.html | 6 +- aircox/views.py | 9 +- .../templates/aircox_cms/diffusion_page.html | 4 +- 9 files changed, 795 insertions(+), 288 deletions(-) diff --git a/aircox/admin.py b/aircox/admin.py index 3ac5ba9..9aefdd8 100755 --- a/aircox/admin.py +++ b/aircox/admin.py @@ -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): diff --git a/aircox/controllers.py b/aircox/controllers.py index e066502..9b158f8 100755 --- a/aircox/controllers.py +++ b/aircox/controllers.py @@ -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() diff --git a/aircox/management/commands/streamer.py b/aircox/management/commands/streamer.py index b237371..1d139bc 100755 --- a/aircox/management/commands/streamer.py +++ b/aircox/management/commands/streamer.py @@ -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) diff --git a/aircox/middleware.py b/aircox/middleware.py index 3f2ffb3..4dd4bb0 100644 --- a/aircox/middleware.py +++ b/aircox/middleware.py @@ -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() diff --git a/aircox/models.py b/aircox/models.py index 2b1819c..0fd5f45 100755 --- a/aircox/models.py +++ b/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) - diff --git a/aircox/static/aircox/css/layout.css b/aircox/static/aircox/css/layout.css index 6f0d0bb..ff6f184 100755 --- a/aircox/static/aircox/css/layout.css +++ b/aircox/static/aircox/css/layout.css @@ -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; } diff --git a/aircox/templates/aircox/controllers/monitor.html b/aircox/templates/aircox/controllers/monitor.html index 660f43a..4ed33d3 100755 --- a/aircox/templates/aircox/controllers/monitor.html +++ b/aircox/templates/aircox/controllers/monitor.html @@ -125,7 +125,7 @@ Monitor.update(50000); {{ station.name }} - {% 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 %} - {% if source.name == station.streamer.current_source.name %} + {% if source.name == station.streamer.source.name %} {% trans {% endif %} {% if source.is_dealer %} @@ -167,7 +167,7 @@ Monitor.update(50000); {% if source.is_dealer %} {{ source.playlist|join:"
" }} {% else %} - {{ source.current_sound }} + {{ source.sound }} {% endif %} diff --git a/aircox/views.py b/aircox/views.py index adf8a5e..af2781a 100755 --- a/aircox/views.py +++ b/aircox/views.py @@ -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 diff --git a/aircox_cms/templates/aircox_cms/diffusion_page.html b/aircox_cms/templates/aircox_cms/diffusion_page.html index 1b24adc..35ff114 100755 --- a/aircox_cms/templates/aircox_cms/diffusion_page.html +++ b/aircox_cms/templates/aircox_cms/diffusion_page.html @@ -19,13 +19,13 @@ {% endif %} {% endwith %} -{% if diffusion.diffusion_set.count %} +{% if diffusion.reruns.count %}

{% trans "Dates of diffusion" %}