forked from rc/aircox
cfr #121 Co-authored-by: Christophe Siraut <d@tobald.eu.org> Co-authored-by: bkfox <thomas bkfox net> Co-authored-by: Thomas Kairos <thomas@bkfox.net> Reviewed-on: rc/aircox#131 Co-authored-by: Chris Tactic <ctactic@noreply.git.radiocampus.be> Co-committed-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
263 lines
7.7 KiB
Python
263 lines
7.7 KiB
Python
import datetime
|
|
import logging
|
|
import operator
|
|
from collections import deque
|
|
|
|
from django.db import models
|
|
from django.utils import timezone as tz
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from .diffusion import Diffusion
|
|
from .sound import Sound
|
|
from .station import Station
|
|
from .track import Track
|
|
from .page import Renderable
|
|
|
|
logger = logging.getLogger("aircox")
|
|
|
|
|
|
__all__ = ("Log", "LogQuerySet")
|
|
|
|
|
|
class LogQuerySet(models.QuerySet):
|
|
def station(self, station=None, id=None):
|
|
return self.filter(station=station) if id is None else self.filter(station_id=id)
|
|
|
|
def date(self, date):
|
|
start = tz.datetime.combine(date, datetime.time())
|
|
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
|
|
return self.filter(date__range=(start, end))
|
|
# this filter does not work with mysql
|
|
# return self.filter(date__date=date)
|
|
|
|
def after(self, date):
|
|
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
|
|
|
|
def before(self, date):
|
|
return self.filter(date__lte=date) if isinstance(date, tz.datetime) else self.filter(date__date__lte=date)
|
|
|
|
def on_air(self):
|
|
return self.filter(type=Log.TYPE_ON_AIR)
|
|
|
|
def start(self):
|
|
return self.filter(type=Log.TYPE_START)
|
|
|
|
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)
|
|
|
|
|
|
class Log(Renderable, models.Model):
|
|
"""Log sounds and diffusions that are played on the station.
|
|
|
|
This only remember what has been played on the outputs, not on each
|
|
source; Source designate here which source is responsible of that.
|
|
"""
|
|
|
|
template_prefix = "log"
|
|
|
|
TYPE_STOP = 0x00
|
|
"""Source has been stopped, e.g. manually."""
|
|
# Rule: \/ diffusion != null \/ sound != null
|
|
TYPE_START = 0x01
|
|
"""Diffusion or sound has been request to be played."""
|
|
TYPE_CANCEL = 0x02
|
|
"""Diffusion has been canceled."""
|
|
# Rule: \/ sound != null /\ track == null
|
|
# \/ sound == null /\ track != null
|
|
# \/ sound == null /\ track == null /\ comment = sound_path
|
|
TYPE_ON_AIR = 0x03
|
|
"""Sound or diffusion occured on air."""
|
|
TYPE_OTHER = 0x04
|
|
"""Other log."""
|
|
TYPE_CHOICES = (
|
|
(TYPE_STOP, _("stop")),
|
|
(TYPE_START, _("start")),
|
|
(TYPE_CANCEL, _("cancelled")),
|
|
(TYPE_ON_AIR, _("on air")),
|
|
(TYPE_OTHER, _("other")),
|
|
)
|
|
|
|
station = models.ForeignKey(
|
|
Station,
|
|
models.CASCADE,
|
|
verbose_name=_("station"),
|
|
help_text=_("related station"),
|
|
)
|
|
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
|
date = models.DateTimeField(_("date"), default=tz.now, db_index=True)
|
|
source = models.CharField(
|
|
# we use a CharField to avoid loosing logs information if the
|
|
# source is removed
|
|
max_length=64,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("source"),
|
|
help_text=_("Identifier of the log's source."),
|
|
)
|
|
comment = models.CharField(
|
|
max_length=512,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("comment"),
|
|
)
|
|
sound = models.ForeignKey(
|
|
Sound,
|
|
models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
db_index=True,
|
|
verbose_name=_("Sound"),
|
|
)
|
|
track = models.ForeignKey(
|
|
Track,
|
|
models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
db_index=True,
|
|
verbose_name=_("Track"),
|
|
)
|
|
diffusion = models.ForeignKey(
|
|
Diffusion,
|
|
models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
db_index=True,
|
|
verbose_name=_("Diffusion"),
|
|
)
|
|
|
|
objects = LogQuerySet.as_manager()
|
|
|
|
@property
|
|
def related(self):
|
|
return self.diffusion or self.sound or self.track
|
|
|
|
# FIXME: required????
|
|
@property
|
|
def local_date(self):
|
|
"""Return a version of self.date that is localized to self.timezone;
|
|
This is needed since datetime are stored as UTC date and we want to get
|
|
it as local time."""
|
|
return tz.localtime(self.date, tz.get_current_timezone())
|
|
|
|
# prepare for the future on crash + ease the use in merged lists with
|
|
# diffusions
|
|
@property
|
|
def start(self):
|
|
return self.date
|
|
|
|
class Meta:
|
|
verbose_name = _("Log")
|
|
verbose_name_plural = _("Logs")
|
|
|
|
def __str__(self):
|
|
return "#{} ({}, {}, {})".format(
|
|
self.pk,
|
|
self.get_type_display(),
|
|
self.source,
|
|
self.local_date.strftime("%Y/%m/%d %H:%M%z"),
|
|
)
|
|
|
|
@classmethod
|
|
def __list_append(cls, object_list, items):
|
|
object_list += [cls(obj) for obj in items]
|
|
|
|
@classmethod
|
|
def merge_diffusions(cls, logs, diffs, count=None, diff_count=None, group_logs=False):
|
|
"""Merge logs and diffusions together.
|
|
|
|
`logs` can either be a queryset or a list ordered by `Log.date`.
|
|
"""
|
|
if isinstance(logs, models.QuerySet):
|
|
logs = list(logs.order_by("-date"))
|
|
diffs = diffs.on_air().order_by("-start")
|
|
if diff_count:
|
|
diffs = diffs[:diff_count]
|
|
diffs = deque(diffs)
|
|
object_list = []
|
|
|
|
while True:
|
|
if not len(diffs):
|
|
cls._append_logs(object_list, logs, len(logs), group=group_logs)
|
|
break
|
|
|
|
if not len(logs):
|
|
object_list += diffs
|
|
break
|
|
|
|
diff = diffs.popleft()
|
|
|
|
# - takes all logs after diff start
|
|
index = cls._next_index(logs, diff.end, len(logs), pred=operator.le)
|
|
cls._append_logs(object_list, logs, index, group=group_logs)
|
|
|
|
if len(logs):
|
|
# FIXME
|
|
# - last log while diff is running
|
|
# if logs[0].date > diff.start:
|
|
# object_list.append(logs[0])
|
|
|
|
# - skips logs while diff is running
|
|
index = cls._next_index(logs, diff.start, len(logs))
|
|
if index is not None and index > 0:
|
|
logs = logs[index:]
|
|
|
|
# - add diff
|
|
object_list.append(diff)
|
|
|
|
return object_list if count is None else object_list[:count]
|
|
|
|
@classmethod
|
|
def _next_index(cls, items, date, default, pred=operator.lt):
|
|
iter = (i for i, v in enumerate(items) if pred(v.date, date))
|
|
return next(iter, default)
|
|
|
|
@classmethod
|
|
def _append_logs(cls, object_list, logs, count, group=False):
|
|
logs = logs[:count]
|
|
if not logs:
|
|
return object_list
|
|
|
|
if group:
|
|
grouped = cls._group_logs_by_time(logs)
|
|
object_list.extend(grouped)
|
|
else:
|
|
object_list += logs
|
|
return object_list
|
|
|
|
@classmethod
|
|
def _group_logs_by_time(cls, logs):
|
|
last_time = -1
|
|
cum = []
|
|
for log in logs:
|
|
hour = log.date.time().hour
|
|
if hour != last_time:
|
|
if cum:
|
|
yield cum
|
|
cum = []
|
|
last_time = hour
|
|
# reverse from lowest to highest date
|
|
cum.insert(0, log)
|
|
if cum:
|
|
yield cum
|
|
|
|
def print(self):
|
|
r = []
|
|
if self.diffusion:
|
|
r.append("diff: " + str(self.diffusion_id))
|
|
if self.sound:
|
|
r.append("sound: " + str(self.sound_id))
|
|
if self.track:
|
|
r.append("track: " + str(self.track_id))
|
|
logger.info(
|
|
"log %s: %s%s",
|
|
str(self),
|
|
self.comment or "",
|
|
" (" + ", ".join(r) + ")" if r else "",
|
|
)
|