358 lines
11 KiB
Python
358 lines
11 KiB
Python
import datetime
|
|
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.utils import timezone as tz
|
|
from django.utils.functional import cached_property
|
|
from django.utils.translation import gettext_lazy as _
|
|
from easy_thumbnails.files import get_thumbnailer
|
|
|
|
from aircox import settings, utils
|
|
|
|
from .page import Page
|
|
from .program import (
|
|
BaseRerun,
|
|
BaseRerunQuerySet,
|
|
ProgramChildQuerySet,
|
|
Schedule,
|
|
)
|
|
|
|
__all__ = ("Episode", "Diffusion", "DiffusionQuerySet")
|
|
|
|
|
|
class Episode(Page):
|
|
objects = ProgramChildQuerySet.as_manager()
|
|
detail_url_name = "episode-detail"
|
|
item_template_name = "aircox/widgets/episode_item.html"
|
|
|
|
@property
|
|
def program(self):
|
|
return getattr(self.parent, "program", None)
|
|
|
|
@cached_property
|
|
def podcasts(self):
|
|
"""Return serialized data about podcasts."""
|
|
from ..serializers import PodcastSerializer
|
|
|
|
podcasts = [
|
|
PodcastSerializer(s).data
|
|
for s in self.sound_set.public().order_by("type")
|
|
]
|
|
if self.cover:
|
|
options = {"size": (128, 128), "crop": "scale"}
|
|
cover = get_thumbnailer(self.cover).get_thumbnail(options).url
|
|
else:
|
|
cover = None
|
|
|
|
for index, podcast in enumerate(podcasts):
|
|
podcasts[index]["cover"] = cover
|
|
podcasts[index]["page_url"] = self.get_absolute_url()
|
|
podcasts[index]["page_title"] = self.title
|
|
return podcasts
|
|
|
|
@program.setter
|
|
def program(self, value):
|
|
self.parent = value
|
|
|
|
class Meta:
|
|
verbose_name = _("Episode")
|
|
verbose_name_plural = _("Episodes")
|
|
|
|
def get_absolute_url(self):
|
|
if not self.is_published:
|
|
return self.program.get_absolute_url()
|
|
return super().get_absolute_url()
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.parent is None:
|
|
raise ValueError("missing parent program")
|
|
super().save(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def get_default_title(cls, page, date):
|
|
return settings.AIRCOX_EPISODE_TITLE.format(
|
|
program=page,
|
|
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
|
|
)
|
|
|
|
@classmethod
|
|
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
|
|
"""Get default Episode's title."""
|
|
title = (
|
|
settings.AIRCOX_EPISODE_TITLE.format(
|
|
program=page,
|
|
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
|
|
)
|
|
if title is None
|
|
else title
|
|
)
|
|
return super().get_init_kwargs_from(
|
|
page, title=title, program=page, **kwargs
|
|
)
|
|
|
|
|
|
class DiffusionQuerySet(BaseRerunQuerySet):
|
|
def episode(self, episode=None, id=None):
|
|
"""Diffusions for this episode."""
|
|
return (
|
|
self.filter(episode=episode)
|
|
if id is None
|
|
else self.filter(episode__id=id)
|
|
)
|
|
|
|
def on_air(self):
|
|
"""On air diffusions."""
|
|
return self.filter(type=Diffusion.TYPE_ON_AIR)
|
|
|
|
# TODO: rename to `datetime`
|
|
def now(self, now=None, order=True):
|
|
"""Diffusions occuring now."""
|
|
now = now or tz.now()
|
|
qs = self.filter(start__lte=now, end__gte=now).distinct()
|
|
return qs.order_by("start") if order else qs
|
|
|
|
def date(self, date=None, order=True):
|
|
"""Diffusions occuring date."""
|
|
date = date or datetime.date.today()
|
|
start = tz.datetime.combine(date, datetime.time())
|
|
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
|
|
# start = tz.get_current_timezone().localize(start)
|
|
# end = tz.get_current_timezone().localize(end)
|
|
qs = self.filter(start__range=(start, end))
|
|
return qs.order_by("start") if order else qs
|
|
|
|
def at(self, date, order=True):
|
|
"""Return diffusions at specified date or datetime."""
|
|
return (
|
|
self.now(date, order)
|
|
if isinstance(date, tz.datetime)
|
|
else self.date(date, order)
|
|
)
|
|
|
|
def after(self, date=None):
|
|
"""Return a queryset of diffusions that happen after the given date
|
|
(default: today)."""
|
|
date = utils.date_or_default(date)
|
|
if isinstance(date, tz.datetime):
|
|
qs = self.filter(Q(start__gte=date) | Q(end__gte=date))
|
|
else:
|
|
qs = self.filter(Q(start__date__gte=date) | Q(end__date__gte=date))
|
|
return qs.order_by("start")
|
|
|
|
def before(self, date=None):
|
|
"""Return a queryset of diffusions that finish before the given date
|
|
(default: today)."""
|
|
date = utils.date_or_default(date)
|
|
if isinstance(date, tz.datetime):
|
|
qs = self.filter(start__lt=date)
|
|
else:
|
|
qs = self.filter(start__date__lt=date)
|
|
return qs.order_by("start")
|
|
|
|
def range(self, start, end):
|
|
# FIXME can return dates that are out of range...
|
|
return self.after(start).before(end)
|
|
|
|
|
|
class Diffusion(BaseRerun):
|
|
"""A Diffusion is an occurrence of a Program that is scheduled on the
|
|
station's timetable. It can be a rerun of a previous diffusion. In such a
|
|
case, use rerun's info instead of its own.
|
|
|
|
A Diffusion without any rerun is named Episode (previously, a
|
|
Diffusion was different from an Episode, but in the end, an
|
|
episode only has a name, a linked program, and a list of sounds, so we
|
|
finally merge theme).
|
|
|
|
A Diffusion can have different types:
|
|
- default: simple diffusion that is planified / did occurred
|
|
- unconfirmed: a generated diffusion that has not been confirmed and thus
|
|
is not yet planified
|
|
- cancel: the diffusion has been canceled
|
|
- stop: the diffusion has been manually stopped
|
|
"""
|
|
|
|
objects = DiffusionQuerySet.as_manager()
|
|
|
|
TYPE_ON_AIR = 0x00
|
|
TYPE_UNCONFIRMED = 0x01
|
|
TYPE_CANCEL = 0x02
|
|
TYPE_CHOICES = (
|
|
(TYPE_ON_AIR, _("on air")),
|
|
(TYPE_UNCONFIRMED, _("not confirmed")),
|
|
(TYPE_CANCEL, _("cancelled")),
|
|
)
|
|
|
|
episode = models.ForeignKey(
|
|
Episode,
|
|
models.CASCADE,
|
|
verbose_name=_("episode"),
|
|
)
|
|
schedule = models.ForeignKey(
|
|
Schedule,
|
|
models.CASCADE,
|
|
verbose_name=_("schedule"),
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
type = models.SmallIntegerField(
|
|
verbose_name=_("type"),
|
|
default=TYPE_ON_AIR,
|
|
choices=TYPE_CHOICES,
|
|
)
|
|
start = models.DateTimeField(_("start"), db_index=True)
|
|
end = models.DateTimeField(_("end"), db_index=True)
|
|
# port = models.ForeignKey(
|
|
# 'self',
|
|
# verbose_name = _('port'),
|
|
# blank = True, null = True,
|
|
# on_delete=models.SET_NULL,
|
|
# help_text = _('use this input port'),
|
|
# )
|
|
|
|
item_template_name = "aircox/widgets/diffusion_item.html"
|
|
|
|
class Meta:
|
|
verbose_name = _("Diffusion")
|
|
verbose_name_plural = _("Diffusions")
|
|
permissions = (
|
|
("programming", _("edit the diffusions' planification")),
|
|
)
|
|
|
|
def __str__(self):
|
|
str_ = "{episode} - {date}".format(
|
|
episode=self.episode and self.episode.title,
|
|
date=self.local_start.strftime("%Y/%m/%d %H:%M%z"),
|
|
)
|
|
if self.initial:
|
|
str_ += " ({})".format(_("rerun"))
|
|
return str_
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
if self.is_initial and self.episode != self._initial["episode"]:
|
|
self.rerun_set.update(episode=self.episode, program=self.program)
|
|
|
|
# def save(self, no_check=False, *args, **kwargs):
|
|
# if self.start != self._initial['start'] or \
|
|
# self.end != self._initial['end']:
|
|
# self.check_conflicts()
|
|
|
|
def save_rerun(self):
|
|
self.episode = self.initial.episode
|
|
self.program = self.episode.program
|
|
|
|
def save_initial(self):
|
|
self.program = self.episode.program
|
|
|
|
@property
|
|
def duration(self):
|
|
return self.end - self.start
|
|
|
|
@property
|
|
def date(self):
|
|
"""Return diffusion start as a date."""
|
|
|
|
return utils.cast_date(self.start)
|
|
|
|
@cached_property
|
|
def local_start(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.start, tz.get_current_timezone())
|
|
|
|
@property
|
|
def local_end(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.end, tz.get_current_timezone())
|
|
|
|
@property
|
|
def is_now(self):
|
|
"""True if diffusion is currently running."""
|
|
now = tz.now()
|
|
return (
|
|
self.type == self.TYPE_ON_AIR
|
|
and self.start <= now
|
|
and self.end >= now
|
|
)
|
|
|
|
@property
|
|
def is_live(self):
|
|
"""True if Diffusion is live (False if there are sounds files)."""
|
|
return (
|
|
self.type == self.TYPE_ON_AIR
|
|
and not self.episode.sound_set.archive().count()
|
|
)
|
|
|
|
def get_playlist(self, **types):
|
|
"""Returns sounds as a playlist (list of *local* archive file path).
|
|
|
|
The given arguments are passed to ``get_sounds``.
|
|
"""
|
|
from .sound import Sound
|
|
|
|
return list(
|
|
self.get_sounds(**types)
|
|
.filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
|
|
.values_list("path", flat=True)
|
|
)
|
|
|
|
def get_sounds(self, **types):
|
|
"""Return a queryset of sounds related to this diffusion, ordered by
|
|
type then path.
|
|
|
|
**types: filter on the given sound types name, as `archive=True`
|
|
"""
|
|
from .sound import Sound
|
|
|
|
sounds = (self.initial or self).sound_set.order_by("type", "path")
|
|
_in = [
|
|
getattr(Sound.Type, name) for name, value in types.items() if value
|
|
]
|
|
|
|
return sounds.filter(type__in=_in)
|
|
|
|
def is_date_in_range(self, date=None):
|
|
"""Return true if the given date is in the diffusion's start-end
|
|
range."""
|
|
date = date or tz.now()
|
|
|
|
return self.start < date < self.end
|
|
|
|
def get_conflicts(self):
|
|
"""Return conflicting diffusions queryset."""
|
|
|
|
# conflicts=Diffusion.objects.filter(
|
|
# Q(start__lt=OuterRef('start'), end__gt=OuterRef('end')) |
|
|
# Q(start__gt=OuterRef('start'), start__lt=OuterRef('end'))
|
|
# )
|
|
# diffs= Diffusion.objects.annotate(conflict_with=Exists(conflicts))
|
|
# .filter(conflict_with=True)
|
|
return (
|
|
Diffusion.objects.filter(
|
|
Q(start__lt=self.start, end__gt=self.start)
|
|
| Q(start__gt=self.start, start__lt=self.end)
|
|
)
|
|
.exclude(pk=self.pk)
|
|
.distinct()
|
|
)
|
|
|
|
def check_conflicts(self):
|
|
conflicts = self.get_conflicts()
|
|
self.conflicts.set(conflicts)
|
|
|
|
_initial = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._initial = {
|
|
"start": self.start,
|
|
"end": self.end,
|
|
"episode": getattr(self, "episode", None),
|
|
}
|