forked from rc/aircox
		
	fix bug in streamer; clean-up; wider sidebar
This commit is contained in:
		@ -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
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        on_air = None
 | 
					        log = self.log_qs.on_air().filter(
 | 
				
			||||||
 | 
					            source = source.id, sound__path = sound_path,
 | 
				
			||||||
 | 
					            date__range = air_times,
 | 
				
			||||||
 | 
					        ).last()
 | 
				
			||||||
        if log:
 | 
					        if log:
 | 
				
			||||||
            # we always check difference in sound info
 | 
					            return log
 | 
				
			||||||
            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.
 | 
					        # get sound
 | 
				
			||||||
            # in some cases, there can be a gap between liquidsoap on_air and
 | 
					        sound = Sound.objects.filter(path = sound_path) \
 | 
				
			||||||
            # log's date; to avoid duplicate we allow a difference of 5 seconds
 | 
					                     .select_related('diffusion').first()
 | 
				
			||||||
            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
 | 
					        diff = None
 | 
				
			||||||
            if last_diff and not last_diff.is_expired():
 | 
					        if sound and sound.diffusion:
 | 
				
			||||||
                archives = last_diff.diffusion.sounds(archive = True)
 | 
					            diff = sound.diffusion.original
 | 
				
			||||||
                if archives.filter(pk = sound.pk).exists():
 | 
					            # check for reruns
 | 
				
			||||||
                    diff = last_diff.diffusion
 | 
					            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
 | 
					        # log sound on air
 | 
				
			||||||
            log = self.log(
 | 
					        return self.log(
 | 
				
			||||||
            type = Log.Type.on_air,
 | 
					            type = Log.Type.on_air,
 | 
				
			||||||
                source = current_source.id,
 | 
					            source = source.id,
 | 
				
			||||||
                date = on_air or tz.now(),
 | 
					            date = source.on_air,
 | 
				
			||||||
            sound = sound,
 | 
					            sound = sound,
 | 
				
			||||||
            diffusion = diff,
 | 
					            diffusion = diff,
 | 
				
			||||||
            # if sound is removed, we keep sound path info
 | 
					            # if sound is removed, we keep sound path info
 | 
				
			||||||
                comment = current_sound,
 | 
					            comment = sound_path,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # trace tracks
 | 
					 | 
				
			||||||
        self.trace_sound_tracks(log)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def trace_tracks(self, 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,7 +251,7 @@ 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):
 | 
				
			||||||
@ -282,7 +259,7 @@ class Monitor:
 | 
				
			|||||||
            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')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -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 %}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user