\S+)", output)
value = value and value.groupdict()
if value:
try:
- value = float(value.get('value'))
+ value = float(value.get("value"))
except ValueError:
value = None
self.values[attr] = value
- self.values['length'] = self.values['Length s']
+ self.values["length"] = self.values["Length s"]
def analyse(self, path, at=None, length=None):
- """
- If at and length are given use them as excerpt to analyse.
- """
- args = ['sox', path, '-n']
+ """If at and length are given use them as excerpt to analyse."""
+ args = ["sox", path, "-n"]
if at is not None and length is not None:
- args += ['trim', str(at), str(length)]
+ args += ["trim", str(at), str(length)]
- args.append('stats')
+ args.append("stats")
- p = subprocess.Popen(args, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
+ p = subprocess.Popen(
+ args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
# sox outputs to stderr (my god WHYYYY)
out_, out = p.communicate()
- self.parse(str(out, encoding='utf-8'))
+ self.parse(str(out, encoding="utf-8"))
class SoundStats:
- path = None # file path
- sample_length = 120 # default sample length in seconds
- stats = None # list of samples statistics
- bad = None # list of bad samples
- good = None # list of good samples
+ path = None # file path
+ sample_length = 120 # default sample length in seconds
+ stats = None # list of samples statistics
+ bad = None # list of bad samples
+ good = None # list of good samples
def __init__(self, path, sample_length=None):
self.path = path
- self.sample_length = sample_length if sample_length is not None \
- else self.sample_length
+ self.sample_length = (
+ sample_length if sample_length is not None else self.sample_length
+ )
def get_file_stats(self):
return self.stats and self.stats[0]
def analyse(self):
- logger.debug('complete file analysis')
+ logger.debug("complete file analysis")
self.stats = [SoxStats(self.path)]
position = 0
- length = self.stats[0].get('length')
+ length = self.stats[0].get("length")
if not self.sample_length:
return
- logger.debug('start samples analysis...')
+ logger.debug("start samples analysis...")
while position < length:
stats = SoxStats(self.path, at=position, length=self.sample_length)
self.stats.append(stats)
position += self.sample_length
def check(self, name, min_val, max_val):
- self.good = [index for index, stats in enumerate(self.stats)
- if min_val <= stats.get(name) <= max_val]
- self.bad = [index for index, stats in enumerate(self.stats)
- if index not in self.good]
+ self.good = [
+ index
+ for index, stats in enumerate(self.stats)
+ if min_val <= stats.get(name) <= max_val
+ ]
+ self.bad = [
+ index
+ for index, stats in enumerate(self.stats)
+ if index not in self.good
+ ]
self.resume()
def resume(self):
- def view(array): return [
- 'file' if index == 0 else
- 'sample {} (at {} seconds)'.format(
- index, (index-1) * self.sample_length)
- for index in array
- ]
+ def view(array):
+ return [
+ "file"
+ if index == 0
+ else "sample {} (at {} seconds)".format(
+ index, (index - 1) * self.sample_length
+ )
+ for index in array
+ ]
if self.good:
- logger.debug(self.path + ' -> good: \033[92m%s\033[0m',
- ', '.join(view(self.good)))
+ logger.debug(
+ self.path + " -> good: \033[92m%s\033[0m",
+ ", ".join(view(self.good)),
+ )
if self.bad:
- logger.debug(self.path + ' -> bad: \033[91m%s\033[0m',
- ', '.join(view(self.bad)))
+ logger.debug(
+ self.path + " -> bad: \033[91m%s\033[0m",
+ ", ".join(view(self.bad)),
+ )
diff --git a/aircox/middleware.py b/aircox/middleware.py
index b64f004..4062826 100644
--- a/aircox/middleware.py
+++ b/aircox/middleware.py
@@ -5,13 +5,13 @@ from django.utils import timezone as tz
from .models import Station
from .utils import Redirect
-
-__all__ = ['AircoxMiddleware']
+__all__ = ("AircoxMiddleware",)
class AircoxMiddleware(object):
- """
- Middleware used to get default info for the given website. Theses
+ """Middleware used to get default info for the given website.
+
+ Theses
This middleware must be set after the middleware
'django.contrib.auth.middleware.AuthenticationMiddleware',
"""
@@ -20,11 +20,11 @@ class AircoxMiddleware(object):
self.get_response = get_response
def get_station(self, request):
- """ Return station for the provided request """
+ """Return station for the provided request."""
expr = Q(default=True) | Q(hosts__contains=request.get_host())
# case = Case(When(hosts__contains=request.get_host(), then=Value(0)),
# When(default=True, then=Value(32)))
- return Station.objects.filter(expr).order_by('default').first()
+ return Station.objects.filter(expr).order_by("default").first()
# .annotate(resolve_priority=case) \
# .order_by('resolve_priority').first()
@@ -33,10 +33,10 @@ class AircoxMiddleware(object):
# required
timezone = None
try:
- timezone = request.session.get('aircox.timezone')
+ timezone = request.session.get("aircox.timezone")
if timezone:
timezone = pytz.timezone(timezone)
- except:
+ except Exception:
pass
if not timezone:
diff --git a/aircox/models/__init__.py b/aircox/models/__init__.py
index 25d1eba..4e1419f 100644
--- a/aircox/models/__init__.py
+++ b/aircox/models/__init__.py
@@ -1,12 +1,48 @@
-from .article import *
-from .page import *
-from .program import *
-from .episode import *
-from .log import *
-from .sound import *
-from .station import *
-from .user_settings import *
-
from . import signals
+from .article import Article
+from .episode import Diffusion, DiffusionQuerySet, Episode
+from .log import Log, LogArchiver, LogQuerySet
+from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
+from .program import (
+ BaseRerun,
+ BaseRerunQuerySet,
+ Program,
+ ProgramChildQuerySet,
+ ProgramQuerySet,
+ Schedule,
+ Stream,
+)
+from .sound import Sound, SoundQuerySet, Track
+from .station import Port, Station, StationQuerySet
+from .user_settings import UserSettings
-
+__all__ = (
+ "signals",
+ "Article",
+ "Episode",
+ "Diffusion",
+ "DiffusionQuerySet",
+ "Log",
+ "LogQuerySet",
+ "LogArchiver",
+ "Category",
+ "PageQuerySet",
+ "Page",
+ "StaticPage",
+ "Comment",
+ "NavItem",
+ "Program",
+ "ProgramQuerySet",
+ "Stream",
+ "Schedule",
+ "ProgramChildQuerySet",
+ "BaseRerun",
+ "BaseRerunQuerySet",
+ "Sound",
+ "SoundQuerySet",
+ "Track",
+ "Station",
+ "StationQuerySet",
+ "Port",
+ "UserSettings",
+)
diff --git a/aircox/models/article.py b/aircox/models/article.py
index 40e1b5a..490faa1 100644
--- a/aircox/models/article.py
+++ b/aircox/models/article.py
@@ -3,16 +3,14 @@ from django.utils.translation import gettext_lazy as _
from .page import Page
from .program import ProgramChildQuerySet
-
-__all__ = ('Article',)
+__all__ = ("Article",)
class Article(Page):
- detail_url_name = 'article-detail'
+ detail_url_name = "article-detail"
objects = ProgramChildQuerySet.as_manager()
class Meta:
- verbose_name = _('Article')
- verbose_name_plural = _('Articles')
-
+ verbose_name = _("Article")
+ verbose_name_plural = _("Articles")
diff --git a/aircox/models/episode.py b/aircox/models/episode.py
index 6773c4e..397dae8 100644
--- a/aircox/models/episode.py
+++ b/aircox/models/episode.py
@@ -3,45 +3,51 @@ import datetime
from django.db import models
from django.db.models import Q
from django.utils import timezone as tz
-from django.utils.translation import gettext_lazy as _
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 .program import ProgramChildQuerySet, \
- BaseRerun, BaseRerunQuerySet, Schedule
+
from .page import Page
+from .program import (
+ BaseRerun,
+ BaseRerunQuerySet,
+ ProgramChildQuerySet,
+ Schedule,
+)
-
-__all__ = ('Episode', 'Diffusion', 'DiffusionQuerySet')
+__all__ = ("Episode", "Diffusion", "DiffusionQuerySet")
class Episode(Page):
objects = ProgramChildQuerySet.as_manager()
- detail_url_name = 'episode-detail'
- item_template_name = 'aircox/widgets/episode_item.html'
+ detail_url_name = "episode-detail"
+ item_template_name = "aircox/widgets/episode_item.html"
@property
def program(self):
- return getattr(self.parent, 'program', None)
+ return getattr(self.parent, "program", None)
@cached_property
def podcasts(self):
- """ Return serialized data about podcasts. """
+ """Return serialized data about podcasts."""
from ..serializers import PodcastSerializer
- podcasts = [PodcastSerializer(s).data
- for s in self.sound_set.public().order_by('type')]
+
+ podcasts = [
+ PodcastSerializer(s).data
+ for s in self.sound_set.public().order_by("type")
+ ]
if self.cover:
- options = {'size': (128, 128), 'crop': 'scale'}
+ 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
+ podcasts[index]["cover"] = cover
+ podcasts[index]["page_url"] = self.get_absolute_url()
+ podcasts[index]["page_title"] = self.title
return podcasts
@program.setter
@@ -49,8 +55,8 @@ class Episode(Page):
self.parent = value
class Meta:
- verbose_name = _('Episode')
- verbose_name_plural = _('Episodes')
+ verbose_name = _("Episode")
+ verbose_name_plural = _("Episodes")
def get_absolute_url(self):
if not self.is_published:
@@ -59,82 +65,89 @@ class Episode(Page):
def save(self, *args, **kwargs):
if self.parent is None:
- raise ValueError('missing parent program')
+ 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)
+ 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)
+ """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)
+ """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 """
+ """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 """
+ """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
+ return qs.order_by("start") if order else qs
def date(self, date=None, order=True):
- """ Diffusions occuring date. """
+ """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
+ 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)
+ """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).
- """
+ """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')
+ return qs.order_by("start")
def before(self, date=None):
- """
- Return a queryset of diffusions that finish before the given
- date (default: today).
- """
+ """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')
+ return qs.order_by("start")
def range(self, start, end):
# FIXME can return dates that are out of range...
@@ -142,10 +155,9 @@ class DiffusionQuerySet(BaseRerunQuerySet):
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 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
@@ -159,29 +171,37 @@ class Diffusion(BaseRerun):
- 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')),
+ (TYPE_ON_AIR, _("on air")),
+ (TYPE_UNCONFIRMED, _("not confirmed")),
+ (TYPE_CANCEL, _("cancelled")),
)
episode = models.ForeignKey(
- Episode, models.CASCADE, verbose_name=_('episode'),
+ Episode,
+ models.CASCADE,
+ verbose_name=_("episode"),
)
schedule = models.ForeignKey(
- Schedule, models.CASCADE, verbose_name=_('schedule'),
- blank=True, null=True,
+ Schedule,
+ models.CASCADE,
+ verbose_name=_("schedule"),
+ blank=True,
+ null=True,
)
type = models.SmallIntegerField(
- verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES,
+ verbose_name=_("type"),
+ default=TYPE_ON_AIR,
+ choices=TYPE_CHOICES,
)
- start = models.DateTimeField(_('start'), db_index=True)
- end = models.DateTimeField(_('end'), db_index=True)
+ start = models.DateTimeField(_("start"), db_index=True)
+ end = models.DateTimeField(_("end"), db_index=True)
# port = models.ForeignKey(
# 'self',
# verbose_name = _('port'),
@@ -190,33 +210,33 @@ class Diffusion(BaseRerun):
# help_text = _('use this input port'),
# )
- item_template_name = 'aircox/widgets/diffusion_item.html'
+ item_template_name = "aircox/widgets/diffusion_item.html"
class Meta:
- verbose_name = _('Diffusion')
- verbose_name_plural = _('Diffusions')
+ verbose_name = _("Diffusion")
+ verbose_name_plural = _("Diffusions")
permissions = (
- ('programming', _('edit the diffusions\' planification')),
+ ("programming", _("edit the diffusions' planification")),
)
def __str__(self):
- str_ = '{episode} - {date}'.format(
+ str_ = "{episode} - {date}".format(
episode=self.episode and self.episode.title,
- date=self.local_start.strftime('%Y/%m/%d %H:%M%z'),
+ date=self.local_start.strftime("%Y/%m/%d %H:%M%z"),
)
if self.initial:
- str_ += ' ({})'.format(_('rerun'))
+ str_ += " ({})".format(_("rerun"))
return str_
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
- if self.is_initial and self.episode != self._initial['episode']:
+ 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(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
@@ -231,85 +251,96 @@ class Diffusion(BaseRerun):
@property
def date(self):
- """ Return diffusion start as a date. """
+ """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 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 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 """
+ """True if diffusion is currently running."""
now = tz.now()
- return self.type == self.TYPE_ON_AIR and \
- self.start <= now and self.end >= 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()
+ """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).
+ """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))
+
+ 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.
+ """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]
+
+ 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.
- """
+ """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 """
+ """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()
+ # 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()
@@ -320,7 +351,7 @@ class Diffusion(BaseRerun):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._initial = {
- 'start': self.start,
- 'end': self.end,
- 'episode': getattr(self, 'episode', None),
+ "start": self.start,
+ "end": self.end,
+ "episode": getattr(self, "episode", None),
}
diff --git a/aircox/models/log.py b/aircox/models/log.py
index 73f4459..199c4d9 100644
--- a/aircox/models/log.py
+++ b/aircox/models/log.py
@@ -1,32 +1,34 @@
-from collections import deque
import datetime
import gzip
import logging
import os
+from collections import deque
import yaml
-
from django.db import models
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import settings
+
from .episode import Diffusion
from .sound import Sound, Track
from .station import Station
-
-logger = logging.getLogger('aircox')
+logger = logging.getLogger("aircox")
-__all__ = ('Log', 'LogQuerySet', 'LogArchiver')
+__all__ = ("Log", "LogQuerySet", "LogArchiver")
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)
+ 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())
@@ -36,9 +38,11 @@ class LogQuerySet(models.QuerySet):
# 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)
+ return (
+ self.filter(date__gte=date)
+ if isinstance(date, tz.datetime)
+ else self.filter(date__date__gte=date)
+ )
def on_air(self):
return self.filter(type=Log.TYPE_ON_AIR)
@@ -57,64 +61,80 @@ class LogQuerySet(models.QuerySet):
class Log(models.Model):
- """
- Log sounds and diffusions that are played on the station.
+ """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.
"""
TYPE_STOP = 0x00
- """ Source has been stopped, e.g. manually """
+ """Source has been stopped, e.g. manually."""
# Rule: \/ diffusion != null \/ sound != null
TYPE_START = 0x01
- """ Diffusion or sound has been request to be played. """
+ """Diffusion or sound has been request to be played."""
TYPE_CANCEL = 0x02
- """ Diffusion has been canceled. """
+ """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 """
+ """Sound or diffusion occured on air."""
TYPE_OTHER = 0x04
- """ Other log """
+ """Other log."""
TYPE_CHOICES = (
- (TYPE_STOP, _('stop')), (TYPE_START, _('start')),
- (TYPE_CANCEL, _('cancelled')), (TYPE_ON_AIR, _('on air')),
- (TYPE_OTHER, _('other'))
+ (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'),
+ 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)
+ 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 source related to this log'),
+ max_length=64,
+ blank=True,
+ null=True,
+ verbose_name=_("source"),
+ help_text=_("identifier of the source related to this log"),
)
comment = models.CharField(
- max_length=512, blank=True, null=True,
- verbose_name=_('comment'),
+ 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'),
+ 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'),
+ 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'),
+ Diffusion,
+ models.SET_NULL,
+ blank=True,
+ null=True,
+ db_index=True,
+ verbose_name=_("Diffusion"),
)
objects = LogQuerySet.as_manager()
@@ -126,11 +146,9 @@ class Log(models.Model):
# 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 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
@@ -140,13 +158,16 @@ class Log(models.Model):
return self.date
class Meta:
- verbose_name = _('Log')
- verbose_name_plural = _('Logs')
+ 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'))
+ 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):
@@ -154,15 +175,15 @@ class Log(models.Model):
@classmethod
def merge_diffusions(cls, logs, diffs, count=None):
- """
- Merge logs and diffusions together. `logs` can either be a queryset
- or a list ordered by `Log.date`.
+ """Merge logs and diffusions together.
+
+ `logs` can either be a queryset or a list ordered by `Log.date`.
"""
# TODO: limit count
# FIXME: log may be iterable (in stats view)
if isinstance(logs, models.QuerySet):
- logs = list(logs.order_by('-date'))
- diffs = deque(diffs.on_air().before().order_by('-start'))
+ logs = list(logs.order_by("-date"))
+ diffs = deque(diffs.on_air().before().order_by("-start"))
object_list = []
while True:
@@ -177,8 +198,10 @@ class Log(models.Model):
diff = diffs.popleft()
# - takes all logs after diff start
- index = next((i for i, v in enumerate(logs)
- if v.date <= diff.end), len(logs))
+ index = next(
+ (i for i, v in enumerate(logs) if v.date <= diff.end),
+ len(logs),
+ )
if index is not None and index > 0:
object_list += logs[:index]
logs = logs[index:]
@@ -186,12 +209,14 @@ class Log(models.Model):
if len(logs):
# FIXME
# - last log while diff is running
- #if logs[0].date > diff.start:
+ # if logs[0].date > diff.start:
# object_list.append(logs[0])
# - skips logs while diff is running
- index = next((i for i, v in enumerate(logs)
- if v.date < diff.start), len(logs))
+ index = next(
+ (i for i, v in enumerate(logs) if v.date < diff.start),
+ len(logs),
+ )
if index is not None and index > 0:
logs = logs[index:]
@@ -203,18 +228,22 @@ class Log(models.Model):
def print(self):
r = []
if self.diffusion:
- r.append('diff: ' + str(self.diffusion_id))
+ r.append("diff: " + str(self.diffusion_id))
if self.sound:
- r.append('sound: ' + str(self.sound_id))
+ 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 '')
-
+ r.append("track: " + str(self.track_id))
+ logger.info(
+ "log %s: %s%s",
+ str(self),
+ self.comment or "",
+ " (" + ", ".join(r) + ")" if r else "",
+ )
class LogArchiver:
- """ Commodity class used to manage archives of logs. """
+ """Commodity class used to manage archives of logs."""
+
@cached_property
def fields(self):
return Log._meta.get_fields()
@@ -223,13 +252,14 @@ class LogArchiver:
def get_path(station, date):
return os.path.join(
settings.AIRCOX_LOGS_ARCHIVES_DIR,
- '{}_{}.log.gz'.format(date.strftime("%Y%m%d"), station.pk)
+ "{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
)
def archive(self, qs, keep=False):
- """
- Archive logs of the given queryset. Delete archived logs if not
- `keep`. Return the count of archived logs
+ """Archive logs of the given queryset.
+
+ Delete archived logs if not `keep`. Return the count of archived
+ logs
"""
if not qs.exists():
return 0
@@ -242,8 +272,10 @@ class LogArchiver:
# exists yet <3
for (station, date), logs in logs.items():
path = self.get_path(station, date)
- with gzip.open(path, 'ab') as archive:
- data = yaml.dump([self.serialize(l) for l in logs]).encode('utf8')
+ with gzip.open(path, "ab") as archive:
+ data = yaml.dump(
+ [self.serialize(line) for line in logs]
+ ).encode("utf8")
archive.write(data)
if not keep:
@@ -253,11 +285,9 @@ class LogArchiver:
@staticmethod
def sort_logs(qs):
- """
- Sort logs by station and date and return a dict of
- `{ (station,date): [logs] }`.
- """
- qs = qs.order_by('date')
+ """Sort logs by station and date and return a dict of `{
+ (station,date): [logs] }`."""
+ qs = qs.order_by("date")
logs = {}
for log in qs:
key = (log.station, log.date)
@@ -268,44 +298,45 @@ class LogArchiver:
return logs
def serialize(self, log):
- """ Serialize log """
- return {i.attname: getattr(log, i.attname)
- for i in self.fields}
+ """Serialize log."""
+ return {i.attname: getattr(log, i.attname) for i in self.fields}
def load(self, station, date):
- """ Load an archive returning logs in a list. """
+ """Load an archive returning logs in a list."""
path = self.get_path(station, date)
if not os.path.exists(path):
return []
- with gzip.open(path, 'rb') as archive:
+ with gzip.open(path, "rb") as archive:
data = archive.read()
logs = yaml.load(data)
# we need to preload diffusions, sounds and tracks
rels = {
- 'diffusion': self.get_relations(logs, Diffusion, 'diffusion'),
- 'sound': self.get_relations(logs, Sound, 'sound'),
- 'track': self.get_relations(logs, Track, 'track'),
+ "diffusion": self.get_relations(logs, Diffusion, "diffusion"),
+ "sound": self.get_relations(logs, Sound, "sound"),
+ "track": self.get_relations(logs, Track, "track"),
}
def rel_obj(log, attr):
- rel_id = log.get(attr + '_id')
+ rel_id = log.get(attr + "_id")
return rels[attr][rel_id] if rel_id else None
- return [Log(diffusion=rel_obj(log, 'diffusion'),
- sound=rel_obj(log, 'sound'),
- track=rel_obj(log, 'track'),
- **log) for log in logs]
+ return [
+ Log(
+ diffusion=rel_obj(log, "diffusion"),
+ sound=rel_obj(log, "sound"),
+ track=rel_obj(log, "track"),
+ **log
+ )
+ for log in logs
+ ]
@staticmethod
def get_relations(logs, model, attr):
- """
- From a list of dict representing logs, retrieve related objects
- of the given type.
- """
- attr_id = attr + '_id'
+ """From a list of dict representing logs, retrieve related objects of
+ the given type."""
+ attr_id = attr + "_id"
pks = (log[attr_id] for log in logs if attr_id in log)
return {rel.pk: rel for rel in model.objects.filter(pk__in=pks)}
-
diff --git a/aircox/models/page.py b/aircox/models/page.py
index bfb8193..c159ebe 100644
--- a/aircox/models/page.py
+++ b/aircox/models/page.py
@@ -1,38 +1,42 @@
import re
-from django.db import models
-from django.urls import reverse
-from django.utils import timezone as tz
-from django.utils.text import slugify
-from django.utils.html import format_html
-from django.utils.safestring import mark_safe
-from django.utils.translation import gettext_lazy as _
-from django.utils.functional import cached_property
-
import bleach
from ckeditor_uploader.fields import RichTextUploadingField
+from django.db import models
+from django.urls import reverse
+from django.utils import timezone as tz
+from django.utils.functional import cached_property
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from django.utils.text import slugify
+from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from model_utils.managers import InheritanceQuerySet
from .station import Station
-
-__all__ = ('Category', 'PageQuerySet',
- 'Page', 'StaticPage', 'Comment', 'NavItem')
+__all__ = (
+ "Category",
+ "PageQuerySet",
+ "Page",
+ "StaticPage",
+ "Comment",
+ "NavItem",
+)
-headline_re = re.compile(r'()?'
- r'(?P[^\n]{1,140}(\n|[^\.]*?\.))'
- r'(
)?')
+headline_re = re.compile(
+ r"()?" r"(?P[^\n]{1,140}(\n|[^\.]*?\.))" r"(
)?"
+)
class Category(models.Model):
- title = models.CharField(_('title'), max_length=64)
- slug = models.SlugField(_('slug'), max_length=64, db_index=True)
+ title = models.CharField(_("title"), max_length=64)
+ slug = models.SlugField(_("slug"), max_length=64, db_index=True)
class Meta:
- verbose_name = _('Category')
- verbose_name_plural = _('Categories')
+ verbose_name = _("Category")
+ verbose_name_plural = _("Categories")
def __str__(self):
return self.title
@@ -49,68 +53,90 @@ class BasePageQuerySet(InheritanceQuerySet):
return self.filter(status=Page.STATUS_TRASH)
def parent(self, parent=None, id=None):
- """ Return pages having this parent. """
- return self.filter(parent=parent) if id is None else \
- self.filter(parent__id=id)
+ """Return pages having this parent."""
+ return (
+ self.filter(parent=parent)
+ if id is None
+ else self.filter(parent__id=id)
+ )
def search(self, q, search_content=True):
if search_content:
- return self.filter(models.Q(title__icontains=q) | models.Q(content__icontains=q))
+ return self.filter(
+ models.Q(title__icontains=q) | models.Q(content__icontains=q)
+ )
return self.filter(title__icontains=q)
class BasePage(models.Model):
- """ Base class for publishable content """
+ """Base class for publishable content."""
+
STATUS_DRAFT = 0x00
STATUS_PUBLISHED = 0x10
STATUS_TRASH = 0x20
STATUS_CHOICES = (
- (STATUS_DRAFT, _('draft')),
- (STATUS_PUBLISHED, _('published')),
- (STATUS_TRASH, _('trash')),
+ (STATUS_DRAFT, _("draft")),
+ (STATUS_PUBLISHED, _("published")),
+ (STATUS_TRASH, _("trash")),
)
- parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True,
- db_index=True, related_name='child_set')
+ parent = models.ForeignKey(
+ "self",
+ models.CASCADE,
+ blank=True,
+ null=True,
+ db_index=True,
+ related_name="child_set",
+ )
title = models.CharField(max_length=100)
- slug = models.SlugField(_('slug'), max_length=120, blank=True, unique=True,
- db_index=True)
+ slug = models.SlugField(
+ _("slug"), max_length=120, blank=True, unique=True, db_index=True
+ )
status = models.PositiveSmallIntegerField(
- _('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES,
+ _("status"),
+ default=STATUS_DRAFT,
+ choices=STATUS_CHOICES,
)
cover = FilerImageField(
on_delete=models.SET_NULL,
- verbose_name=_('cover'), null=True, blank=True,
+ verbose_name=_("cover"),
+ null=True,
+ blank=True,
)
content = RichTextUploadingField(
- _('content'), blank=True, null=True,
+ _("content"),
+ blank=True,
+ null=True,
)
objects = BasePageQuerySet.as_manager()
detail_url_name = None
- item_template_name = 'aircox/widgets/page_item.html'
+ item_template_name = "aircox/widgets/page_item.html"
class Meta:
abstract = True
def __str__(self):
- return '{}'.format(self.title or self.pk)
+ return "{}".format(self.title or self.pk)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)[:100]
count = Page.objects.filter(slug__startswith=self.slug).count()
if count:
- self.slug += '-' + str(count)
+ self.slug += "-" + str(count)
if self.parent and not self.cover:
self.cover = self.parent.cover
super().save(*args, **kwargs)
def get_absolute_url(self):
- return reverse(self.detail_url_name, kwargs={'slug': self.slug}) \
- if self.is_published else '#'
+ return (
+ reverse(self.detail_url_name, kwargs={"slug": self.slug})
+ if self.is_published
+ else "#"
+ )
@property
def is_draft(self):
@@ -133,15 +159,15 @@ class BasePage(models.Model):
@cached_property
def headline(self):
if not self.content:
- return ''
+ return ""
content = bleach.clean(self.content, tags=[], strip=True)
headline = headline_re.search(content)
- return mark_safe(headline.groupdict()['headline']) if headline else ''
+ return mark_safe(headline.groupdict()["headline"]) if headline else ""
@classmethod
def get_init_kwargs_from(cls, page, **kwargs):
- kwargs.setdefault('cover', page.cover)
- kwargs.setdefault('category', page.category)
+ kwargs.setdefault("cover", page.cover)
+ kwargs.setdefault("category", page.category)
return kwargs
@classmethod
@@ -151,30 +177,39 @@ class BasePage(models.Model):
class PageQuerySet(BasePageQuerySet):
def published(self):
- return self.filter(status=Page.STATUS_PUBLISHED,
- pub_date__lte=tz.now())
+ return self.filter(
+ status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now()
+ )
class Page(BasePage):
- """ Base Page model used for articles and other dated content. """
+ """Base Page model used for articles and other dated content."""
+
category = models.ForeignKey(
- Category, models.SET_NULL,
- verbose_name=_('category'), blank=True, null=True, db_index=True
+ Category,
+ models.SET_NULL,
+ verbose_name=_("category"),
+ blank=True,
+ null=True,
+ db_index=True,
)
pub_date = models.DateTimeField(
- _('publication date'), blank=True, null=True, db_index=True)
+ _("publication date"), blank=True, null=True, db_index=True
+ )
featured = models.BooleanField(
- _('featured'), default=False,
+ _("featured"),
+ default=False,
)
allow_comments = models.BooleanField(
- _('allow comments'), default=True,
+ _("allow comments"),
+ default=True,
)
objects = PageQuerySet.as_manager()
class Meta:
- verbose_name = _('Publication')
- verbose_name_plural = _('Publications')
+ verbose_name = _("Publication")
+ verbose_name_plural = _("Publications")
def save(self, *args, **kwargs):
if self.is_published and self.pub_date is None:
@@ -188,8 +223,9 @@ class Page(BasePage):
class StaticPage(BasePage):
- """ Static page that eventually can be attached to a specific view. """
- detail_url_name = 'static-page-detail'
+ """Static page that eventually can be attached to a specific view."""
+
+ detail_url_name = "static-page-detail"
ATTACH_TO_HOME = 0x00
ATTACH_TO_DIFFUSIONS = 0x01
@@ -199,25 +235,28 @@ class StaticPage(BasePage):
ATTACH_TO_ARTICLES = 0x05
ATTACH_TO_CHOICES = (
- (ATTACH_TO_HOME, _('Home page')),
- (ATTACH_TO_DIFFUSIONS, _('Diffusions page')),
- (ATTACH_TO_LOGS, _('Logs page')),
- (ATTACH_TO_PROGRAMS, _('Programs list')),
- (ATTACH_TO_EPISODES, _('Episodes list')),
- (ATTACH_TO_ARTICLES, _('Articles list')),
+ (ATTACH_TO_HOME, _("Home page")),
+ (ATTACH_TO_DIFFUSIONS, _("Diffusions page")),
+ (ATTACH_TO_LOGS, _("Logs page")),
+ (ATTACH_TO_PROGRAMS, _("Programs list")),
+ (ATTACH_TO_EPISODES, _("Episodes list")),
+ (ATTACH_TO_ARTICLES, _("Articles list")),
)
VIEWS = {
- ATTACH_TO_HOME: 'home',
- ATTACH_TO_DIFFUSIONS: 'diffusion-list',
- ATTACH_TO_LOGS: 'log-list',
- ATTACH_TO_PROGRAMS: 'program-list',
- ATTACH_TO_EPISODES: 'episode-list',
- ATTACH_TO_ARTICLES: 'article-list',
+ ATTACH_TO_HOME: "home",
+ ATTACH_TO_DIFFUSIONS: "diffusion-list",
+ ATTACH_TO_LOGS: "log-list",
+ ATTACH_TO_PROGRAMS: "program-list",
+ ATTACH_TO_EPISODES: "episode-list",
+ ATTACH_TO_ARTICLES: "article-list",
}
attach_to = models.SmallIntegerField(
- _('attach to'), choices=ATTACH_TO_CHOICES, blank=True, null=True,
- help_text=_('display this page content to related element'),
+ _("attach to"),
+ choices=ATTACH_TO_CHOICES,
+ blank=True,
+ null=True,
+ help_text=_("display this page content to related element"),
)
def get_absolute_url(self):
@@ -228,49 +267,65 @@ class StaticPage(BasePage):
class Comment(models.Model):
page = models.ForeignKey(
- Page, models.CASCADE, verbose_name=_('related page'),
+ Page,
+ models.CASCADE,
+ verbose_name=_("related page"),
db_index=True,
# TODO: allow_comment filter
)
- nickname = models.CharField(_('nickname'), max_length=32)
- email = models.EmailField(_('email'), max_length=32)
+ nickname = models.CharField(_("nickname"), max_length=32)
+ email = models.EmailField(_("email"), max_length=32)
date = models.DateTimeField(auto_now_add=True)
- content = models.TextField(_('content'), max_length=1024)
+ content = models.TextField(_("content"), max_length=1024)
class Meta:
- verbose_name = _('Comment')
- verbose_name_plural = _('Comments')
+ verbose_name = _("Comment")
+ verbose_name_plural = _("Comments")
class NavItem(models.Model):
- """ Navigation menu items """
+ """Navigation menu items."""
+
station = models.ForeignKey(
- Station, models.CASCADE, verbose_name=_('station'))
- menu = models.SlugField(_('menu'), max_length=24)
- order = models.PositiveSmallIntegerField(_('order'))
- text = models.CharField(_('title'), max_length=64)
- url = models.CharField(_('url'), max_length=256, blank=True, null=True)
- page = models.ForeignKey(StaticPage, models.CASCADE, db_index=True,
- verbose_name=_('page'), blank=True, null=True)
+ Station, models.CASCADE, verbose_name=_("station")
+ )
+ menu = models.SlugField(_("menu"), max_length=24)
+ order = models.PositiveSmallIntegerField(_("order"))
+ text = models.CharField(_("title"), max_length=64)
+ url = models.CharField(_("url"), max_length=256, blank=True, null=True)
+ page = models.ForeignKey(
+ StaticPage,
+ models.CASCADE,
+ db_index=True,
+ verbose_name=_("page"),
+ blank=True,
+ null=True,
+ )
+
class Meta:
- verbose_name = _('Menu item')
- verbose_name_plural = _('Menu items')
- ordering = ('order', 'pk')
+ verbose_name = _("Menu item")
+ verbose_name_plural = _("Menu items")
+ ordering = ("order", "pk")
def get_url(self):
- return self.url if self.url else \
- self.page.get_absolute_url() if self.page else None
+ return (
+ self.url
+ if self.url
+ else self.page.get_absolute_url()
+ if self.page
+ else None
+ )
- def render(self, request, css_class='', active_class=''):
+ def render(self, request, css_class="", active_class=""):
url = self.get_url()
if active_class and request.path.startswith(url):
- css_class += ' ' + active_class
+ css_class += " " + active_class
if not url:
return self.text
elif not css_class:
return format_html('{}', url, self.text)
else:
- return format_html('{}', url,
- css_class, self.text)
-
+ return format_html(
+ '{}', url, css_class, self.text
+ )
diff --git a/aircox/models/program.py b/aircox/models/program.py
index 4b6c1e0..9354237 100644
--- a/aircox/models/program.py
+++ b/aircox/models/program.py
@@ -1,9 +1,9 @@
import calendar
-from collections import OrderedDict
-from enum import IntEnum
import logging
import os
import shutil
+from collections import OrderedDict
+from enum import IntEnum
import pytz
from django.conf import settings as conf
@@ -12,19 +12,26 @@ from django.db import models
from django.db.models import F
from django.db.models.functions import Concat, Substr
from django.utils import timezone as tz
-from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
+from django.utils.translation import gettext_lazy as _
from aircox import settings, utils
+
from .page import Page, PageQuerySet
from .station import Station
-
-logger = logging.getLogger('aircox')
+logger = logging.getLogger("aircox")
-__all__ = ('Program', 'ProgramQuerySet', 'Stream', 'Schedule',
- 'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet')
+__all__ = (
+ "Program",
+ "ProgramQuerySet",
+ "Stream",
+ "Schedule",
+ "ProgramChildQuerySet",
+ "BaseRerun",
+ "BaseRerunQuerySet",
+)
class ProgramQuerySet(PageQuerySet):
@@ -37,8 +44,7 @@ class ProgramQuerySet(PageQuerySet):
class Program(Page):
- """
- A Program can either be a Streamed or a Scheduled program.
+ """A Program can either be a Streamed or a Scheduled program.
A Streamed program is used to generate non-stop random playlists when there
is not scheduled diffusion. In such a case, a Stream is used to describe
@@ -49,32 +55,35 @@ class Program(Page):
Renaming a Program rename the corresponding directory to matches the new
name if it does not exists.
"""
+
# explicit foreign key in order to avoid related name clashes
- station = models.ForeignKey(Station, models.CASCADE,
- verbose_name=_('station'))
+ station = models.ForeignKey(
+ Station, models.CASCADE, verbose_name=_("station")
+ )
active = models.BooleanField(
- _('active'),
+ _("active"),
default=True,
- help_text=_('if not checked this program is no longer active')
+ help_text=_("if not checked this program is no longer active"),
)
sync = models.BooleanField(
- _('syncronise'),
+ _("syncronise"),
default=True,
- help_text=_('update later diffusions according to schedule changes')
+ help_text=_("update later diffusions according to schedule changes"),
)
objects = ProgramQuerySet.as_manager()
- detail_url_name = 'program-detail'
+ detail_url_name = "program-detail"
@property
def path(self):
- """ Return program's directory path """
- return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
- self.slug.replace('-', '_'))
+ """Return program's directory path."""
+ return os.path.join(
+ settings.AIRCOX_PROGRAMS_DIR, self.slug.replace("-", "_")
+ )
@property
def abspath(self):
- """ Return absolute path to program's dir """
+ """Return absolute path to program's dir."""
return os.path.join(conf.MEDIA_ROOT, self.path)
@property
@@ -93,69 +102,88 @@ class Program(Page):
@classmethod
def get_from_path(cl, path):
- """
- Return a Program from the given path. We assume the path has been
- given in a previous time by this model (Program.path getter).
+ """Return a Program from the given path.
+
+ We assume the path has been given in a previous time by this
+ model (Program.path getter).
"""
if path.startswith(settings.AIRCOX_PROGRAMS_DIR_ABS):
- path = path.replace(settings.AIRCOX_PROGRAMS_DIR_ABS, '')
- while path[0] == '/':
+ path = path.replace(settings.AIRCOX_PROGRAMS_DIR_ABS, "")
+ while path[0] == "/":
path = path[1:]
- path = path[:path.index('/')]
- return cl.objects.filter(slug=path.replace('_','-')).first()
+ path = path[: path.index("/")]
+ return cl.objects.filter(slug=path.replace("_", "-")).first()
def ensure_dir(self, subdir=None):
+ """Make sur the program's dir exists (and optionally subdir).
+
+ Return True if the dir (or subdir) exists.
"""
- Make sur the program's dir exists (and optionally subdir). Return True
- if the dir (or subdir) exists.
- """
- path = os.path.join(self.abspath, subdir) if subdir else \
- self.abspath
+ path = os.path.join(self.abspath, subdir) if subdir else self.abspath
os.makedirs(path, exist_ok=True)
return os.path.exists(path)
class Meta:
- verbose_name = _('Program')
- verbose_name_plural = _('Programs')
+ verbose_name = _("Program")
+ verbose_name_plural = _("Programs")
def __str__(self):
return self.title
def save(self, *kargs, **kwargs):
from .sound import Sound
+
super().save(*kargs, **kwargs)
# TODO: move in signals
- path_ = getattr(self, '__initial_path', None)
+ path_ = getattr(self, "__initial_path", None)
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
- if path_ is not None and path_ != self.path and \
- os.path.exists(abspath) and not os.path.exists(self.abspath):
- logger.info('program #%s\'s dir changed to %s - update it.',
- self.id, self.title)
+ if (
+ path_ is not None
+ and path_ != self.path
+ and os.path.exists(abspath)
+ and not os.path.exists(self.abspath)
+ ):
+ logger.info(
+ "program #%s's dir changed to %s - update it.",
+ self.id,
+ self.title,
+ )
shutil.move(abspath, self.abspath)
- Sound.objects.filter(path__startswith=path_) \
- .update(file=Concat('file', Substr(F('file'), len(path_))))
+ Sound.objects.filter(path__startswith=path_).update(
+ file=Concat("file", Substr(F("file"), len(path_)))
+ )
class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None):
- return self.filter(parent__program__station=station) if id is None else \
- self.filter(parent__program__station__id=id)
+ return (
+ self.filter(parent__program__station=station)
+ if id is None
+ else self.filter(parent__program__station__id=id)
+ )
def program(self, program=None, id=None):
return self.parent(program, id)
class BaseRerunQuerySet(models.QuerySet):
- """ Queryset for BaseRerun (sub)classes. """
+ """Queryset for BaseRerun (sub)classes."""
+
def station(self, station=None, id=None):
- return self.filter(program__station=station) if id is None else \
- self.filter(program__station__id=id)
+ return (
+ self.filter(program__station=station)
+ if id is None
+ else self.filter(program__station__id=id)
+ )
def program(self, program=None, id=None):
- return self.filter(program=program) if id is None else \
- self.filter(program__id=id)
+ return (
+ self.filter(program=program)
+ if id is None
+ else self.filter(program__id=id)
+ )
def rerun(self):
return self.filter(initial__isnull=False)
@@ -165,19 +193,27 @@ class BaseRerunQuerySet(models.QuerySet):
class BaseRerun(models.Model):
+ """Abstract model offering rerun facilities.
+
+ Assume `start` is a datetime field or attribute implemented by
+ subclass.
"""
- Abstract model offering rerun facilities. Assume `start` is a
- datetime field or attribute implemented by subclass.
- """
+
program = models.ForeignKey(
- Program, models.CASCADE, db_index=True,
- verbose_name=_('related program'),
+ Program,
+ models.CASCADE,
+ db_index=True,
+ verbose_name=_("related program"),
)
initial = models.ForeignKey(
- 'self', models.SET_NULL, related_name='rerun_set',
- verbose_name=_('rerun of'),
- limit_choices_to={'initial__isnull': True},
- blank=True, null=True, db_index=True,
+ "self",
+ models.SET_NULL,
+ related_name="rerun_set",
+ verbose_name=_("rerun of"),
+ limit_choices_to={"initial__isnull": True},
+ blank=True,
+ null=True,
+ db_index=True,
)
objects = BaseRerunQuerySet.as_manager()
@@ -212,25 +248,27 @@ class BaseRerun(models.Model):
return self.initial is not None
def get_initial(self):
- """ Return the initial schedule (self or initial) """
+ """Return the initial schedule (self or initial)"""
return self if self.initial is None else self.initial.get_initial()
def clean(self):
super().clean()
if self.initial is not None and self.initial.start >= self.start:
- raise ValidationError({
- 'initial': _('rerun must happen after original')
- })
+ raise ValidationError(
+ {"initial": _("rerun must happen after original")}
+ )
# ? BIG FIXME: self.date is still used as datetime
class Schedule(BaseRerun):
+ """A Schedule defines time slots of programs' diffusions.
+
+ It can be an initial run or a rerun (in such case it is linked to
+ the related schedule).
"""
- A Schedule defines time slots of programs' diffusions. It can be an initial
- run or a rerun (in such case it is linked to the related schedule).
- """
- # Frequency for schedules. Basically, it is a mask of bits where each bit is
- # a week. Bits > rank 5 are used for special schedules.
+
+ # Frequency for schedules. Basically, it is a mask of bits where each bit
+ # is a week. Bits > rank 5 are used for special schedules.
# Important: the first week is always the first week where the weekday of
# the schedule is present.
# For ponctual programs, there is no need for a schedule, only a diffusion
@@ -247,45 +285,55 @@ class Schedule(BaseRerun):
one_on_two = 0b100000
date = models.DateField(
- _('date'), help_text=_('date of the first diffusion'),
+ _("date"),
+ help_text=_("date of the first diffusion"),
)
time = models.TimeField(
- _('time'), help_text=_('start time'),
+ _("time"),
+ help_text=_("start time"),
)
timezone = models.CharField(
- _('timezone'),
- default=tz.get_current_timezone, max_length=100,
+ _("timezone"),
+ default=tz.get_current_timezone,
+ max_length=100,
choices=[(x, x) for x in pytz.all_timezones],
- help_text=_('timezone used for the date')
+ help_text=_("timezone used for the date"),
)
duration = models.TimeField(
- _('duration'),
- help_text=_('regular duration'),
+ _("duration"),
+ help_text=_("regular duration"),
)
frequency = models.SmallIntegerField(
- _('frequency'),
- choices=[(int(y), {
- 'ponctual': _('ponctual'),
- 'first': _('1st {day} of the month'),
- 'second': _('2nd {day} of the month'),
- 'third': _('3rd {day} of the month'),
- 'fourth': _('4th {day} of the month'),
- 'last': _('last {day} of the month'),
- 'first_and_third': _('1st and 3rd {day} of the month'),
- 'second_and_fourth': _('2nd and 4th {day} of the month'),
- 'every': _('{day}'),
- 'one_on_two': _('one {day} on two'),
- }[x]) for x, y in Frequency.__members__.items()],
+ _("frequency"),
+ choices=[
+ (
+ int(y),
+ {
+ "ponctual": _("ponctual"),
+ "first": _("1st {day} of the month"),
+ "second": _("2nd {day} of the month"),
+ "third": _("3rd {day} of the month"),
+ "fourth": _("4th {day} of the month"),
+ "last": _("last {day} of the month"),
+ "first_and_third": _("1st and 3rd {day} of the month"),
+ "second_and_fourth": _("2nd and 4th {day} of the month"),
+ "every": _("{day}"),
+ "one_on_two": _("one {day} on two"),
+ }[x],
+ )
+ for x, y in Frequency.__members__.items()
+ ],
)
class Meta:
- verbose_name = _('Schedule')
- verbose_name_plural = _('Schedules')
+ verbose_name = _("Schedule")
+ verbose_name_plural = _("Schedules")
def __str__(self):
- return '{} - {}, {}'.format(
- self.program.title, self.get_frequency_verbose(),
- self.time.strftime('%H:%M')
+ return "{} - {}, {}".format(
+ self.program.title,
+ self.get_frequency_verbose(),
+ self.time.strftime("%H:%M"),
)
def save_rerun(self, *args, **kwargs):
@@ -295,31 +343,35 @@ class Schedule(BaseRerun):
@cached_property
def tz(self):
- """ Pytz timezone of the schedule. """
+ """Pytz timezone of the schedule."""
import pytz
+
return pytz.timezone(self.timezone)
@cached_property
def start(self):
- """ Datetime of the start (timezone unaware) """
+ """Datetime of the start (timezone unaware)"""
return tz.datetime.combine(self.date, self.time)
@cached_property
def end(self):
- """ Datetime of the end """
+ """Datetime of the end."""
return self.start + utils.to_timedelta(self.duration)
def get_frequency_verbose(self):
- """ Return frequency formated for display """
+ """Return frequency formated for display."""
from django.template.defaultfilters import date
- return self.get_frequency_display().format(
- day=date(self.date, 'l')
- ).capitalize()
+
+ return (
+ self.get_frequency_display()
+ .format(day=date(self.date, "l"))
+ .capitalize()
+ )
# initial cached data
__initial = None
- def changed(self, fields=['date', 'duration', 'frequency', 'timezone']):
+ def changed(self, fields=["date", "duration", "frequency", "timezone"]):
initial = self._Schedule__initial
if not initial:
@@ -334,15 +386,13 @@ class Schedule(BaseRerun):
return False
def normalize(self, date):
- """
- Return a datetime set to schedule's time for the provided date,
- handling timezone (based on schedule's timezone).
- """
+ """Return a datetime set to schedule's time for the provided date,
+ handling timezone (based on schedule's timezone)."""
date = tz.datetime.combine(date, self.time)
return self.tz.normalize(self.tz.localize(date))
def dates_of_month(self, date):
- """ Return normalized diffusion dates of provided date's month. """
+ """Return normalized diffusion dates of provided date's month."""
if self.frequency == Schedule.Frequency.ponctual:
return []
@@ -352,7 +402,8 @@ class Schedule(BaseRerun):
# last of the month
if freq == Schedule.Frequency.last:
date = date.replace(
- day=calendar.monthrange(date.year, date.month)[1])
+ day=calendar.monthrange(date.year, date.month)[1]
+ )
date_wday = date.weekday()
# end of month before the wanted weekday: move one week back
@@ -361,56 +412,72 @@ class Schedule(BaseRerun):
date += tz.timedelta(days=sched_wday - date_wday)
return [self.normalize(date)]
- # move to the first day of the month that matches the schedule's weekday
- # check on SO#3284452 for the formula
+ # move to the first day of the month that matches the schedule's
+ # weekday. Check on SO#3284452 for the formula
date_wday, month = date.weekday(), date.month
- date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) -
- date_wday + sched_wday)
+ date += tz.timedelta(
+ days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
+ )
if freq == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
# - there are max 3 "weeks on two" per month
if (date - self.date).days % 14:
date += tz.timedelta(days=7)
- dates = (date + tz.timedelta(days=14*i) for i in range(0, 3))
+ dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
else:
- dates = (date + tz.timedelta(days=7*week) for week in range(0, 5)
- if freq & (0b1 << week))
+ dates = (
+ date + tz.timedelta(days=7 * week)
+ for week in range(0, 5)
+ if freq & (0b1 << week)
+ )
return [self.normalize(date) for date in dates if date.month == month]
-
def _exclude_existing_date(self, dates):
from .episode import Diffusion
- saved = set(Diffusion.objects.filter(start__in=dates)
- .values_list('start', flat=True))
+
+ saved = set(
+ Diffusion.objects.filter(start__in=dates).values_list(
+ "start", flat=True
+ )
+ )
return [date for date in dates if date not in saved]
-
def diffusions_of_month(self, date):
- """
- Get episodes and diffusions for month of provided date, including
+ """Get episodes and diffusions for month of provided date, including
reruns.
+
:returns: tuple([Episode], [Diffusion])
"""
from .episode import Diffusion, Episode
- if self.initial is not None or \
- self.frequency == Schedule.Frequency.ponctual:
+
+ if (
+ self.initial is not None
+ or self.frequency == Schedule.Frequency.ponctual
+ ):
return [], []
# dates for self and reruns as (date, initial)
- reruns = [(rerun, rerun.date - self.date)
- for rerun in self.rerun_set.all()]
+ reruns = [
+ (rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
+ ]
dates = OrderedDict((date, None) for date in self.dates_of_month(date))
- dates.update([(rerun.normalize(date.date() + delta), date)
- for date in dates.keys() for rerun, delta in reruns])
+ dates.update(
+ [
+ (rerun.normalize(date.date() + delta), date)
+ for date in dates.keys()
+ for rerun, delta in reruns
+ ]
+ )
# remove dates corresponding to existing diffusions
- saved = set(Diffusion.objects.filter(start__in=dates.keys(),
- program=self.program,
- schedule=self)
- .values_list('start', flat=True))
+ saved = set(
+ Diffusion.objects.filter(
+ start__in=dates.keys(), program=self.program, schedule=self
+ ).values_list("start", flat=True)
+ )
# make diffs
duration = utils.to_timedelta(self.duration)
@@ -430,8 +497,12 @@ class Schedule(BaseRerun):
initial = diffusions[initial]
diffusions[date] = Diffusion(
- episode=episode, schedule=self, type=Diffusion.TYPE_ON_AIR,
- initial=initial, start=date, end=date+duration
+ episode=episode,
+ schedule=self,
+ type=Diffusion.TYPE_ON_AIR,
+ initial=initial,
+ start=date,
+ end=date + duration,
)
return episodes.values(), diffusions.values()
@@ -440,36 +511,38 @@ class Schedule(BaseRerun):
# TODO/FIXME: use validators?
if self.initial is not None and self.date > self.date:
- raise ValueError('initial must be later')
+ raise ValueError("initial must be later")
class Stream(models.Model):
- """
- When there are no program scheduled, it is possible to play sounds
- in order to avoid blanks. A Stream is a Program that plays this role,
- and whose linked to a Stream.
+ """When there are no program scheduled, it is possible to play sounds in
+ order to avoid blanks. A Stream is a Program that plays this role, and
+ whose linked to a Stream.
All sounds that are marked as good and that are under the related
program's archive dir are elligible for the sound's selection.
"""
+
program = models.ForeignKey(
- Program, models.CASCADE,
- verbose_name=_('related program'),
+ Program,
+ models.CASCADE,
+ verbose_name=_("related program"),
)
delay = models.TimeField(
- _('delay'), blank=True, null=True,
- help_text=_('minimal delay between two sound plays')
+ _("delay"),
+ blank=True,
+ null=True,
+ help_text=_("minimal delay between two sound plays"),
)
begin = models.TimeField(
- _('begin'), blank=True, null=True,
- help_text=_('used to define a time range this stream is '
- 'played')
+ _("begin"),
+ blank=True,
+ null=True,
+ help_text=_("used to define a time range this stream is " "played"),
)
end = models.TimeField(
- _('end'),
- blank=True, null=True,
- help_text=_('used to define a time range this stream is '
- 'played')
+ _("end"),
+ blank=True,
+ null=True,
+ help_text=_("used to define a time range this stream is " "played"),
)
-
-
diff --git a/aircox/models/signals.py b/aircox/models/signals.py
index 886c8bc..a9c9eae 100755
--- a/aircox/models/signals.py
+++ b/aircox/models/signals.py
@@ -1,6 +1,4 @@
-import pytz
-
-from django.contrib.auth.models import User, Group, Permission
+from django.contrib.auth.models import Group, Permission, User
from django.db import transaction
from django.db.models import signals
from django.dispatch import receiver
@@ -18,9 +16,7 @@ from . import Diffusion, Episode, Page, Program, Schedule
#
@receiver(signals.post_save, sender=User)
def user_default_groups(sender, instance, created, *args, **kwargs):
- """
- Set users to different default groups
- """
+ """Set users to different default groups."""
if not created or instance.is_superuser:
return
@@ -32,7 +28,8 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
if created and permissions:
for codename in permissions:
permission = Permission.objects.filter(
- codename=codename).first()
+ codename=codename
+ ).first()
if permission:
group.permissions.add(permission)
group.save()
@@ -42,43 +39,40 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
@receiver(signals.post_save, sender=Page)
def page_post_save(sender, instance, created, *args, **kwargs):
if not created and instance.cover:
- Page.objects.filter(parent=instance, cover__isnull=True) \
- .update(cover=instance.cover)
+ Page.objects.filter(parent=instance, cover__isnull=True).update(
+ cover=instance.cover
+ )
@receiver(signals.post_save, sender=Program)
def program_post_save(sender, instance, created, *args, **kwargs):
- """
- Clean-up later diffusions when a program becomes inactive
- """
+ """Clean-up later diffusions when a program becomes inactive."""
if not instance.active:
Diffusion.object.program(instance).after(tz.now()).delete()
- Episode.object.parent(instance).filter(diffusion__isnull=True) \
- .delete()
+ Episode.object.parent(instance).filter(diffusion__isnull=True).delete()
- cover = getattr(instance, '__initial_cover', None)
+ cover = getattr(instance, "__initial_cover", None)
if cover is None and instance.cover is not None:
- Episode.objects.parent(instance) \
- .filter(cover__isnull=True) \
- .update(cover=instance.cover)
-
+ Episode.objects.parent(instance).filter(cover__isnull=True).update(
+ cover=instance.cover
+ )
@receiver(signals.pre_save, sender=Schedule)
def schedule_pre_save(sender, instance, *args, **kwargs):
- if getattr(instance, 'pk') is not None:
+ if getattr(instance, "pk") is not None:
instance._initial = Schedule.objects.get(pk=instance.pk)
@receiver(signals.post_save, sender=Schedule)
def schedule_post_save(sender, instance, created, *args, **kwargs):
- """
- Handles Schedule's time, duration and timezone changes and update
- corresponding diffusions accordingly.
- """
- initial = getattr(instance, '_initial', None)
- if not initial or ((instance.time, instance.duration, instance.timezone) ==
- (initial.time, initial.duration, initial.timezone)):
+ """Handles Schedule's time, duration and timezone changes and update
+ corresponding diffusions accordingly."""
+ initial = getattr(instance, "_initial", None)
+ if not initial or (
+ (instance.time, instance.duration, instance.timezone)
+ == (initial.time, initial.duration, initial.timezone)
+ ):
return
today = tz.datetime.today()
@@ -94,14 +88,15 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
@receiver(signals.pre_delete, sender=Schedule)
def schedule_pre_delete(sender, instance, *args, **kwargs):
- """ Delete later corresponding diffusion to a changed schedule. """
+ """Delete later corresponding diffusion to a changed schedule."""
Diffusion.objects.filter(schedule=instance).after(tz.now()).delete()
- Episode.objects.filter(diffusion__isnull=True, content__isnull=True,
- sound__isnull=True).delete()
+ Episode.objects.filter(
+ diffusion__isnull=True, content__isnull=True, sound__isnull=True
+ ).delete()
+
@receiver(signals.post_delete, sender=Diffusion)
def diffusion_post_delete(sender, instance, *args, **kwargs):
- Episode.objects.filter(diffusion__isnull=True, content__isnull=True,
- sound__isnull=True).delete()
-
-
+ Episode.objects.filter(
+ diffusion__isnull=True, content__isnull=True, sound__isnull=True
+ ).delete()
diff --git a/aircox/models/sound.py b/aircox/models/sound.py
index 5eb40be..31da7f3 100644
--- a/aircox/models/sound.py
+++ b/aircox/models/sound.py
@@ -6,18 +6,17 @@ from django.db import models
from django.db.models import Q
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
-
from taggit.managers import TaggableManager
from aircox import settings
-from .program import Program
+
from .episode import Episode
+from .program import Program
+
+logger = logging.getLogger("aircox")
-logger = logging.getLogger('aircox')
-
-
-__all__ = ('Sound', 'SoundQuerySet', 'Track')
+__all__ = ("Sound", "SoundQuerySet", "Track")
class SoundQuerySet(models.QuerySet):
@@ -37,122 +36,150 @@ class SoundQuerySet(models.QuerySet):
return self.exclude(type=Sound.TYPE_REMOVED)
def public(self):
- """ Return sounds available as podcasts """
+ """Return sounds available as podcasts."""
return self.filter(is_public=True)
def downloadable(self):
- """ Return sounds available as podcasts """
+ """Return sounds available as podcasts."""
return self.filter(is_downloadable=True)
def archive(self):
- """ Return sounds that are archives """
+ """Return sounds that are archives."""
return self.filter(type=Sound.TYPE_ARCHIVE)
def path(self, paths):
if isinstance(paths, str):
- return self.filter(file=paths.replace(conf.MEDIA_ROOT + '/', ''))
- return self.filter(file__in=(p.replace(conf.MEDIA_ROOT + '/', '')
- for p in paths))
+ return self.filter(file=paths.replace(conf.MEDIA_ROOT + "/", ""))
+ return self.filter(
+ file__in=(p.replace(conf.MEDIA_ROOT + "/", "") for p in paths)
+ )
def playlist(self, archive=True, order_by=True):
- """
- Return files absolute paths as a flat list (exclude sound without path).
+ """Return files absolute paths as a flat list (exclude sound without
+ path).
+
If `order_by` is True, order by path.
"""
if archive:
self = self.archive()
if order_by:
- self = self.order_by('file')
- return [os.path.join(conf.MEDIA_ROOT, file) for file in self.filter(file__isnull=False) \
- .values_list('file', flat=True)]
+ self = self.order_by("file")
+ return [
+ os.path.join(conf.MEDIA_ROOT, file)
+ for file in self.filter(file__isnull=False).values_list(
+ "file", flat=True
+ )
+ ]
def search(self, query):
return self.filter(
- Q(name__icontains=query) | Q(file__icontains=query) |
- Q(program__title__icontains=query) |
- Q(episode__title__icontains=query)
+ Q(name__icontains=query)
+ | Q(file__icontains=query)
+ | Q(program__title__icontains=query)
+ | Q(episode__title__icontains=query)
)
# TODO:
# - provide a default name based on program and episode
class Sound(models.Model):
- """
- A Sound is the representation of a sound file that can be either an excerpt
- or a complete archive of the related diffusion.
- """
+ """A Sound is the representation of a sound file that can be either an
+ excerpt or a complete archive of the related diffusion."""
+
TYPE_OTHER = 0x00
TYPE_ARCHIVE = 0x01
TYPE_EXCERPT = 0x02
TYPE_REMOVED = 0x03
TYPE_CHOICES = (
- (TYPE_OTHER, _('other')), (TYPE_ARCHIVE, _('archive')),
- (TYPE_EXCERPT, _('excerpt')), (TYPE_REMOVED, _('removed'))
+ (TYPE_OTHER, _("other")),
+ (TYPE_ARCHIVE, _("archive")),
+ (TYPE_EXCERPT, _("excerpt")),
+ (TYPE_REMOVED, _("removed")),
)
- name = models.CharField(_('name'), max_length=64)
+ name = models.CharField(_("name"), max_length=64)
program = models.ForeignKey(
- Program, models.CASCADE, blank=True, # NOT NULL
- verbose_name=_('program'),
- help_text=_('program related to it'),
+ Program,
+ models.CASCADE,
+ blank=True, # NOT NULL
+ verbose_name=_("program"),
+ help_text=_("program related to it"),
db_index=True,
)
episode = models.ForeignKey(
- Episode, models.SET_NULL, blank=True, null=True,
- verbose_name=_('episode'),
+ Episode,
+ models.SET_NULL,
+ blank=True,
+ null=True,
+ verbose_name=_("episode"),
db_index=True,
)
- type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
+ type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
position = models.PositiveSmallIntegerField(
- _('order'), default=0, help_text=_('position in the playlist'),
+ _("order"),
+ default=0,
+ help_text=_("position in the playlist"),
)
def _upload_to(self, filename):
- subdir = settings.AIRCOX_SOUND_ARCHIVES_SUBDIR \
- if self.type == self.TYPE_ARCHIVE else \
- settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
+ subdir = (
+ settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
+ if self.type == self.TYPE_ARCHIVE
+ else settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
+ )
return os.path.join(self.program.path, subdir, filename)
file = models.FileField(
- _('file'), upload_to=_upload_to, max_length=256,
- db_index=True, unique=True,
+ _("file"),
+ upload_to=_upload_to,
+ max_length=256,
+ db_index=True,
+ unique=True,
)
duration = models.TimeField(
- _('duration'),
- blank=True, null=True,
- help_text=_('duration of the sound'),
+ _("duration"),
+ blank=True,
+ null=True,
+ help_text=_("duration of the sound"),
)
mtime = models.DateTimeField(
- _('modification time'),
- blank=True, null=True,
- help_text=_('last modification date and time'),
+ _("modification time"),
+ blank=True,
+ null=True,
+ help_text=_("last modification date and time"),
)
is_good_quality = models.BooleanField(
- _('good quality'), help_text=_('sound meets quality requirements'),
- blank=True, null=True
+ _("good quality"),
+ help_text=_("sound meets quality requirements"),
+ blank=True,
+ null=True,
)
is_public = models.BooleanField(
- _('public'), help_text=_('whether it is publicly available as podcast'),
+ _("public"),
+ help_text=_("whether it is publicly available as podcast"),
default=False,
)
is_downloadable = models.BooleanField(
- _('downloadable'),
- help_text=_('whether it can be publicly downloaded by visitors (sound must be public)'),
+ _("downloadable"),
+ help_text=_(
+ "whether it can be publicly downloaded by visitors (sound must be "
+ "public)"
+ ),
default=False,
)
objects = SoundQuerySet.as_manager()
class Meta:
- verbose_name = _('Sound')
- verbose_name_plural = _('Sounds')
+ verbose_name = _("Sound")
+ verbose_name_plural = _("Sounds")
@property
def url(self):
return self.file and self.file.url
def __str__(self):
- return '/'.join(self.file.path.split('/')[-3:])
+ return "/".join(self.file.path.split("/")[-3:])
def save(self, check=True, *args, **kwargs):
if self.episode is not None and self.program is None:
@@ -166,29 +193,28 @@ class Sound(models.Model):
# TODO: rename get_file_mtime(self)
def get_mtime(self):
- """
- Get the last modification date from file
- """
+ """Get the last modification date from file."""
mtime = os.stat(self.file.path).st_mtime
mtime = tz.datetime.fromtimestamp(mtime)
mtime = mtime.replace(microsecond=0)
return tz.make_aware(mtime, tz.get_current_timezone())
def file_exists(self):
- """ Return true if the file still exists. """
+ """Return true if the file still exists."""
return os.path.exists(self.file.path)
# TODO: rename to sync_fs()
def check_on_file(self):
- """
- Check sound file info again'st self, and update informations if
- needed (do not save). Return True if there was changes.
+ """Check sound file info again'st self, and update informations if
+ needed (do not save).
+
+ Return True if there was changes.
"""
if not self.file_exists():
if self.type == self.TYPE_REMOVED:
return
- logger.debug('sound %s: has been removed', self.file.name)
+ logger.debug("sound %s: has been removed", self.file.name)
self.type = self.TYPE_REMOVED
return True
@@ -197,9 +223,11 @@ class Sound(models.Model):
if self.type == self.TYPE_REMOVED and self.program:
changed = True
- self.type = self.TYPE_ARCHIVE \
- if self.file.name.startswith(self.program.archives_path) else \
- self.TYPE_EXCERPT
+ self.type = (
+ self.TYPE_ARCHIVE
+ if self.file.name.startswith(self.program.archives_path)
+ else self.TYPE_EXCERPT
+ )
# check mtime -> reset quality if changed (assume file changed)
mtime = self.get_mtime()
@@ -207,8 +235,10 @@ class Sound(models.Model):
if self.mtime != mtime:
self.mtime = mtime
self.is_good_quality = None
- logger.debug('sound %s: m_time has changed. Reset quality info',
- self.file.name)
+ logger.debug(
+ "sound %s: m_time has changed. Reset quality info",
+ self.file.name,
+ )
return True
return changed
@@ -218,7 +248,7 @@ class Sound(models.Model):
# FIXME: later, remove date?
name = os.path.basename(self.file.name)
name = os.path.splitext(name)[0]
- self.name = name.replace('_', ' ').strip()
+ self.name = name.replace("_", " ").strip()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -226,53 +256,67 @@ class Sound(models.Model):
class Track(models.Model):
+ """Track of a playlist of an object.
+
+ The position can either be expressed as the position in the playlist
+ or as the moment in seconds it started.
"""
- Track of a playlist of an object. The position can either be expressed
- as the position in the playlist or as the moment in seconds it started.
- """
+
episode = models.ForeignKey(
- Episode, models.CASCADE, blank=True, null=True,
- verbose_name=_('episode'),
+ Episode,
+ models.CASCADE,
+ blank=True,
+ null=True,
+ verbose_name=_("episode"),
)
sound = models.ForeignKey(
- Sound, models.CASCADE, blank=True, null=True,
- verbose_name=_('sound'),
+ Sound,
+ models.CASCADE,
+ blank=True,
+ null=True,
+ verbose_name=_("sound"),
)
position = models.PositiveSmallIntegerField(
- _('order'), default=0, help_text=_('position in the playlist'),
+ _("order"),
+ default=0,
+ help_text=_("position in the playlist"),
)
timestamp = models.PositiveSmallIntegerField(
- _('timestamp'),
- blank=True, null=True,
- help_text=_('position (in seconds)')
+ _("timestamp"),
+ blank=True,
+ null=True,
+ help_text=_("position (in seconds)"),
)
- title = models.CharField(_('title'), max_length=128)
- artist = models.CharField(_('artist'), max_length=128)
- album = models.CharField(_('album'), max_length=128, null=True, blank=True)
- tags = TaggableManager(verbose_name=_('tags'), blank=True)
- year = models.IntegerField(_('year'), blank=True, null=True)
+ title = models.CharField(_("title"), max_length=128)
+ artist = models.CharField(_("artist"), max_length=128)
+ album = models.CharField(_("album"), max_length=128, null=True, blank=True)
+ tags = TaggableManager(verbose_name=_("tags"), blank=True)
+ year = models.IntegerField(_("year"), blank=True, null=True)
# FIXME: remove?
info = models.CharField(
- _('information'),
+ _("information"),
max_length=128,
- blank=True, null=True,
- help_text=_('additional informations about this track, such as '
- 'the version, if is it a remix, features, etc.'),
+ blank=True,
+ null=True,
+ help_text=_(
+ "additional informations about this track, such as "
+ "the version, if is it a remix, features, etc."
+ ),
)
class Meta:
- verbose_name = _('Track')
- verbose_name_plural = _('Tracks')
- ordering = ('position',)
+ verbose_name = _("Track")
+ verbose_name_plural = _("Tracks")
+ ordering = ("position",)
def __str__(self):
- return '{self.artist} -- {self.title} -- {self.position}'.format(
- self=self)
+ return "{self.artist} -- {self.title} -- {self.position}".format(
+ self=self
+ )
def save(self, *args, **kwargs):
- if (self.sound is None and self.episode is None) or \
- (self.sound is not None and self.episode is not None):
- raise ValueError('sound XOR episode is required')
+ if (self.sound is None and self.episode is None) or (
+ self.sound is not None and self.episode is not None
+ ):
+ raise ValueError("sound XOR episode is required")
super().save(*args, **kwargs)
-
-
diff --git a/aircox/models/station.py b/aircox/models/station.py
index 3528abd..3dd3509 100644
--- a/aircox/models/station.py
+++ b/aircox/models/station.py
@@ -1,25 +1,20 @@
import os
from django.db import models
-from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
-
+from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from .. import settings
-
-__all__ = ('Station', 'StationQuerySet', 'Port')
+__all__ = ("Station", "StationQuerySet", "Port")
class StationQuerySet(models.QuerySet):
def default(self, station=None):
- """
- Return station model instance, using defaults or
- given one.
- """
+ """Return station model instance, using defaults or given one."""
if station is None:
- return self.order_by('-default', 'pk').first()
+ return self.order_by("-default", "pk").first()
return self.filter(pk=station).first()
def active(self):
@@ -27,66 +22,79 @@ class StationQuerySet(models.QuerySet):
class Station(models.Model):
- """
- Represents a radio station, to which multiple programs are attached
- and that is used as the top object for everything.
+ """Represents a radio station, to which multiple programs are attached and
+ that is used as the top object for everything.
A Station holds controllers for the audio stream generation too.
- Theses are set up when needed (at the first access to these elements)
- then cached.
+ Theses are set up when needed (at the first access to these
+ elements) then cached.
"""
- name = models.CharField(_('name'), max_length=64)
- slug = models.SlugField(_('slug'), max_length=64, unique=True)
+
+ name = models.CharField(_("name"), max_length=64)
+ slug = models.SlugField(_("slug"), max_length=64, unique=True)
# FIXME: remove - should be decided only by Streamer controller + settings
path = models.CharField(
- _('path'),
- help_text=_('path to the working directory'),
+ _("path"),
+ help_text=_("path to the working directory"),
max_length=256,
blank=True,
)
default = models.BooleanField(
- _('default station'),
+ _("default station"),
default=False,
- help_text=_('use this station as the main one.')
+ help_text=_("use this station as the main one."),
)
active = models.BooleanField(
- _('active'),
+ _("active"),
default=True,
- help_text=_('whether this station is still active or not.')
+ help_text=_("whether this station is still active or not."),
)
logo = FilerImageField(
- on_delete=models.SET_NULL, null=True, blank=True,
- verbose_name=_('Logo'),
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ verbose_name=_("Logo"),
)
hosts = models.TextField(
- _("website's urls"), max_length=512, null=True, blank=True,
- help_text=_('specify one url per line')
+ _("website's urls"),
+ max_length=512,
+ null=True,
+ blank=True,
+ help_text=_("specify one url per line"),
)
audio_streams = models.TextField(
- _("audio streams"), max_length=2048, null=True, blank=True,
- help_text=_("Audio streams urls used by station's player. One url "
- "a line.")
+ _("audio streams"),
+ max_length=2048,
+ null=True,
+ blank=True,
+ help_text=_(
+ "Audio streams urls used by station's player. One url " "a line."
+ ),
)
default_cover = FilerImageField(
on_delete=models.SET_NULL,
- verbose_name=_('Default pages\' cover'), null=True, blank=True,
- related_name='+',
+ verbose_name=_("Default pages' cover"),
+ null=True,
+ blank=True,
+ related_name="+",
)
objects = StationQuerySet.as_manager()
@cached_property
def streams(self):
- """ Audio streams as list of urls. """
- return self.audio_streams.split('\n') if self.audio_streams else []
+ """Audio streams as list of urls."""
+ return self.audio_streams.split("\n") if self.audio_streams else []
def __str__(self):
return self.name
def save(self, make_sources=True, *args, **kwargs):
if not self.path:
- self.path = os.path.join(settings.AIRCOX_CONTROLLERS_WORKING_DIR,
- self.slug.replace('-', '_'))
+ self.path = os.path.join(
+ settings.AIRCOX_CONTROLLERS_WORKING_DIR,
+ self.slug.replace("-", "_"),
+ )
if self.default:
qs = Station.objects.filter(default=True)
@@ -99,22 +107,20 @@ class Station(models.Model):
class PortQuerySet(models.QuerySet):
def active(self, value=True):
- """ Active ports """
+ """Active ports."""
return self.filter(active=value)
def output(self):
- """ Filter in output ports """
+ """Filter in output ports."""
return self.filter(direction=Port.DIRECTION_OUTPUT)
def input(self):
- """ Fitler in input ports """
+ """Fitler in input ports."""
return self.filter(direction=Port.DIRECTION_INPUT)
class Port(models.Model):
- """
- Represent an audio input/output for the audio stream
- generation.
+ """Represent an audio input/output for the audio stream generation.
You might want to take a look to LiquidSoap's documentation
for the options available for each kind of input/output.
@@ -122,10 +128,13 @@ class Port(models.Model):
Some port types may be not available depending on the
direction of the port.
"""
+
DIRECTION_INPUT = 0x00
DIRECTION_OUTPUT = 0x01
- DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')),
- (DIRECTION_OUTPUT, _('output')))
+ DIRECTION_CHOICES = (
+ (DIRECTION_INPUT, _("input")),
+ (DIRECTION_OUTPUT, _("output")),
+ )
TYPE_JACK = 0x00
TYPE_ALSA = 0x01
@@ -135,27 +144,34 @@ class Port(models.Model):
TYPE_HTTPS = 0x05
TYPE_FILE = 0x06
TYPE_CHOICES = (
- (TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'),
- (TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'),
- (TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'),
- (TYPE_FILE, _('file'))
+ (TYPE_JACK, "jack"),
+ (TYPE_ALSA, "alsa"),
+ (TYPE_PULSEAUDIO, "pulseaudio"),
+ (TYPE_ICECAST, "icecast"),
+ (TYPE_HTTP, "http"),
+ (TYPE_HTTPS, "https"),
+ (TYPE_FILE, _("file")),
)
station = models.ForeignKey(
- Station, models.CASCADE, verbose_name=_('station'))
+ Station, models.CASCADE, verbose_name=_("station")
+ )
direction = models.SmallIntegerField(
- _('direction'), choices=DIRECTION_CHOICES)
- type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
+ _("direction"), choices=DIRECTION_CHOICES
+ )
+ type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
active = models.BooleanField(
- _('active'), default=True,
- help_text=_('this port is active')
+ _("active"), default=True, help_text=_("this port is active")
)
settings = models.TextField(
- _('port settings'),
- help_text=_('list of comma separated params available; '
- 'this is put in the output config file as raw code; '
- 'plugin related'),
- blank=True, null=True
+ _("port settings"),
+ help_text=_(
+ "list of comma separated params available; "
+ "this is put in the output config file as raw code; "
+ "plugin related"
+ ),
+ blank=True,
+ null=True,
)
objects = PortQuerySet.as_manager()
@@ -163,22 +179,17 @@ class Port(models.Model):
def __str__(self):
return "{direction}: {type} #{id}".format(
direction=self.get_direction_display(),
- type=self.get_type_display(), id=self.pk or ''
+ type=self.get_type_display(),
+ id=self.pk or "",
)
def is_valid_type(self):
- """
- Return True if the type is available for the given direction.
- """
+ """Return True if the type is available for the given direction."""
if self.direction == self.DIRECTION_INPUT:
- return self.type not in (
- self.TYPE_ICECAST, self.TYPE_FILE
- )
+ return self.type not in (self.TYPE_ICECAST, self.TYPE_FILE)
- return self.type not in (
- self.TYPE_HTTP, self.TYPE_HTTPS
- )
+ return self.type not in (self.TYPE_HTTP, self.TYPE_HTTPS)
def save(self, *args, **kwargs):
if not self.is_valid_type():
@@ -187,4 +198,3 @@ class Port(models.Model):
)
return super().save(*args, **kwargs)
-
diff --git a/aircox/models/user_settings.py b/aircox/models/user_settings.py
index a7dabfa..c9f0465 100644
--- a/aircox/models/user_settings.py
+++ b/aircox/models/user_settings.py
@@ -1,16 +1,20 @@
-from django.db import models
from django.contrib.auth.models import User
+from django.db import models
from django.utils.translation import gettext_lazy as _
+__all__ = ("UserSettings",)
+
class UserSettings(models.Model):
- """
- Store user's settings.
- """
+ """Store user's settings."""
+
user = models.OneToOneField(
- User, models.CASCADE, verbose_name=_('User'),
- related_name='aircox_settings')
- playlist_editor_columns = models.JSONField(
- _('Playlist Editor Columns'))
+ User,
+ models.CASCADE,
+ verbose_name=_("User"),
+ related_name="aircox_settings",
+ )
+ playlist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
playlist_editor_sep = models.CharField(
- _('Playlist Editor Separator'), max_length=16)
+ _("Playlist Editor Separator"), max_length=16
+ )
diff --git a/aircox/serializers/__init__.py b/aircox/serializers/__init__.py
index d61a535..531a3db 100644
--- a/aircox/serializers/__init__.py
+++ b/aircox/serializers/__init__.py
@@ -1,3 +1,12 @@
-from .log import *
-from .sound import *
-from .admin import *
+from .admin import TrackSerializer, UserSettingsSerializer
+from .log import LogInfo, LogInfoSerializer
+from .sound import PodcastSerializer, SoundSerializer
+
+__all__ = (
+ "TrackSerializer",
+ "UserSettingsSerializer",
+ "LogInfo",
+ "LogInfoSerializer",
+ "SoundSerializer",
+ "PodcastSerializer",
+)
diff --git a/aircox/serializers/admin.py b/aircox/serializers/admin.py
index 34658cb..1fbccef 100644
--- a/aircox/serializers/admin.py
+++ b/aircox/serializers/admin.py
@@ -1,10 +1,9 @@
from rest_framework import serializers
-from taggit.serializers import TagListSerializerField, TaggitSerializer
+from taggit.serializers import TaggitSerializer, TagListSerializerField
from ..models import Track, UserSettings
-
-__all__ = ('TrackSerializer', 'UserSettingsSerializer')
+__all__ = ("TrackSerializer", "UserSettingsSerializer")
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
@@ -12,19 +11,29 @@ class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
class Meta:
model = Track
- fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
- 'info', 'tags', 'episode', 'sound', 'timestamp')
+ fields = (
+ "pk",
+ "artist",
+ "title",
+ "album",
+ "year",
+ "position",
+ "info",
+ "tags",
+ "episode",
+ "sound",
+ "timestamp",
+ )
class UserSettingsSerializer(serializers.ModelSerializer):
# TODO: validate fields values (playlist_editor_columns at least)
class Meta:
model = UserSettings
- fields = ('playlist_editor_columns', 'playlist_editor_sep')
+ fields = ("playlist_editor_columns", "playlist_editor_sep")
def create(self, validated_data):
- user = self.context.get('user')
+ user = self.context.get("user")
if user:
- validated_data['user_id'] = user.id
+ validated_data["user_id"] = user.id
return super().create(validated_data)
-
diff --git a/aircox/serializers/log.py b/aircox/serializers/log.py
index 7f5f68f..0d83c3d 100644
--- a/aircox/serializers/log.py
+++ b/aircox/serializers/log.py
@@ -2,14 +2,13 @@ from rest_framework import serializers
from ..models import Diffusion, Log
-
-__all__ = ('LogInfo', 'LogInfoSerializer')
+__all__ = ("LogInfo", "LogInfoSerializer")
class LogInfo:
obj = None
start, end = None, None
- title, artist = '', ''
+ title, artist = "", ""
url, cover = None, None
info = None
@@ -20,17 +19,17 @@ class LogInfo:
elif isinstance(obj, Log):
self.from_log(obj)
else:
- raise ValueError('`obj` must be a Diffusion or a Track Log.')
+ raise ValueError("`obj` must be a Diffusion or a Track Log.")
@property
def type(self):
- return 'track' if isinstance(self.obj, Log) else 'diffusion'
+ return "track" if isinstance(self.obj, Log) else "diffusion"
def from_diffusion(self, obj):
episode = obj.episode
self.start, self.end = obj.start, obj.end
self.title, self.url = episode.title, episode.get_absolute_url()
- self.cover = episode.cover and episode.cover.icons['64']
+ self.cover = episode.cover and episode.cover.icons["64"]
self.info = episode.category and episode.category.title
self.obj = obj
diff --git a/aircox/serializers/sound.py b/aircox/serializers/sound.py
index 5ad68db..e91ab9c 100644
--- a/aircox/serializers/sound.py
+++ b/aircox/serializers/sound.py
@@ -2,14 +2,27 @@ from rest_framework import serializers
from ..models import Sound
+__all__ = ("SoundSerializer", "PodcastSerializer")
+
class SoundSerializer(serializers.ModelSerializer):
file = serializers.FileField(use_url=False)
class Meta:
model = Sound
- fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
- 'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
+ fields = [
+ "pk",
+ "name",
+ "program",
+ "episode",
+ "type",
+ "file",
+ "duration",
+ "mtime",
+ "is_good_quality",
+ "is_public",
+ "url",
+ ]
class PodcastSerializer(serializers.ModelSerializer):
@@ -17,5 +30,14 @@ class PodcastSerializer(serializers.ModelSerializer):
class Meta:
model = Sound
- fields = ['pk', 'name', 'program', 'episode', 'type',
- 'duration', 'mtime', 'url', 'is_downloadable']
+ fields = [
+ "pk",
+ "name",
+ "program",
+ "episode",
+ "type",
+ "duration",
+ "mtime",
+ "url",
+ "is_downloadable",
+ ]
diff --git a/aircox/settings.py b/aircox/settings.py
index 8fb5614..2f45da6 100755
--- a/aircox/settings.py
+++ b/aircox/settings.py
@@ -2,61 +2,11 @@ import os
from django.conf import settings
-# TODO:
-# - items() iteration
-# - sub-settings as values
-# - validate() settings
-# - Meta inner-class?
-# - custom settings class instead of default
-#class BaseSettings:
-# deprecated = set()
-#
-# def __init__(self, user_conf):
-# if user_conf:
-# for key, value in user_conf.items():
-# if not hasattr(self, key):
-# if key in self.deprecated:
-# raise ValueError('"{}" config is deprecated'.format(key))
-# else:
-# raise ValueError('"{}" is not a config value'.format(key))
-# setattr(self, key, value)
-#
-#
-#class Settings(BaseSettings):
-# default_user_groups = {
-#
-# }
-#
-# programs_dir = os.path.join(settings.MEDIA_ROOT, 'programs'),
-# """ Programs data directory. """
-# episode_title = '{program.title} - {date}'
-# """ Default episodes title. """
-# episode_title_date_format = '%-d %B %Y'
-# """ Date format used in episode title. """
-#
-# logs_archives_dir = os.path.join(settings.PROJECT_ROOT, 'logs/archives')
-# """ Directory where logs are saved once archived """
-# logs_archive_age = 30
-# """ Default age of log before being archived """
-#
-# sounds_default_dir = os.path.join(settings.MEDIA_ROOT, 'programs/defaults')
-# sound_archive_dir = 'archives'
-# sound_excerpt_dir = 'excerpts'
-# sound_quality = {
-# 'attribute': 'RMS lev dB',
-# 'range': (-18.0, -8.0),
-# 'sample_length': 120,
-# }
-# sound_ext = ('.ogg', '.flac', '.wav', '.mp3', '.opus')
-#
-# # TODO: move into aircox_streamer
-# streamer_working_dir = '/tmp/aircox'
-#
-#
-#
def ensure(key, default):
- globals()[key] = getattr(settings, key, default)
+ value = getattr(settings, key, default)
+ globals()[key] = value
+ return value
########################################################################
@@ -64,85 +14,101 @@ def ensure(key, default):
########################################################################
# group to assign to users at their creation, along with the permissions
# to add to each group.
-ensure('AIRCOX_DEFAULT_USER_GROUPS', {
- 'radio hosts': (
- # TODO include content_type in order to avoid clash with potential
- # extra applications
-
- # aircox
- 'change_program', 'change_episode', 'change_diffusion',
- 'add_comment', 'change_comment', 'delete_comment',
- 'add_article', 'change_article', 'delete_article',
- 'change_sound',
- 'add_track', 'change_track', 'delete_track',
-
- # taggit
- 'add_tag', 'change_tag', 'delete_tag',
-
- # filer
- 'add_folder', 'change_folder', 'delete_folder', 'can_use_directory_listing',
- 'add_image', 'change_image', 'delete_image',
- ),
-})
+ensure(
+ "AIRCOX_DEFAULT_USER_GROUPS",
+ {
+ "radio hosts": (
+ # TODO include content_type in order to avoid clash with potential
+ # extra applications
+ # aircox
+ "change_program",
+ "change_episode",
+ "change_diffusion",
+ "add_comment",
+ "change_comment",
+ "delete_comment",
+ "add_article",
+ "change_article",
+ "delete_article",
+ "change_sound",
+ "add_track",
+ "change_track",
+ "delete_track",
+ # taggit
+ "add_tag",
+ "change_tag",
+ "delete_tag",
+ # filer
+ "add_folder",
+ "change_folder",
+ "delete_folder",
+ "can_use_directory_listing",
+ "add_image",
+ "change_image",
+ "delete_image",
+ ),
+ },
+)
# Directory for the programs data
-ensure('AIRCOX_PROGRAMS_DIR', 'programs')
-ensure('AIRCOX_PROGRAMS_DIR_ABS', os.path.join(settings.MEDIA_ROOT,
- AIRCOX_PROGRAMS_DIR))
+AIRCOX_PROGRAMS_DIR = ensure("AIRCOX_PROGRAMS_DIR", "programs")
+ensure(
+ "AIRCOX_PROGRAMS_DIR_ABS",
+ os.path.join(settings.MEDIA_ROOT, AIRCOX_PROGRAMS_DIR),
+)
########################################################################
# Programs & Episodes
########################################################################
# default title for episodes
-ensure('AIRCOX_EPISODE_TITLE', '{program.title} - {date}')
+ensure("AIRCOX_EPISODE_TITLE", "{program.title} - {date}")
# date format in episode title (python's strftime)
-ensure('AIRCOX_EPISODE_TITLE_DATE_FORMAT', '%-d %B %Y')
+ensure("AIRCOX_EPISODE_TITLE_DATE_FORMAT", "%-d %B %Y")
########################################################################
# Logs & Archives
########################################################################
# Directory where to save logs' archives
-ensure('AIRCOX_LOGS_ARCHIVES_DIR', os.path.join(settings.PROJECT_ROOT, 'logs/archives'))
+ensure(
+ "AIRCOX_LOGS_ARCHIVES_DIR",
+ os.path.join(settings.PROJECT_ROOT, "logs/archives"),
+)
# In days, minimal age of a log before it is archived
-ensure('AIRCOX_LOGS_ARCHIVES_AGE', 60)
+ensure("AIRCOX_LOGS_ARCHIVES_AGE", 60)
########################################################################
# Sounds
########################################################################
# Sub directory used for the complete episode sounds
-ensure('AIRCOX_SOUND_ARCHIVES_SUBDIR', 'archives')
+ensure("AIRCOX_SOUND_ARCHIVES_SUBDIR", "archives")
# Sub directory used for the excerpts of the episode
-ensure('AIRCOX_SOUND_EXCERPTS_SUBDIR', 'excerpts')
+ensure("AIRCOX_SOUND_EXCERPTS_SUBDIR", "excerpts")
# Quality attributes passed to sound_quality_check from sounds_monitor
-ensure('AIRCOX_SOUND_QUALITY', {
- 'attribute': 'RMS lev dB',
- 'range': (-18.0, -8.0),
- 'sample_length': 120,
-}
+ensure(
+ "AIRCOX_SOUND_QUALITY",
+ {
+ "attribute": "RMS lev dB",
+ "range": (-18.0, -8.0),
+ "sample_length": 120,
+ },
)
# Extension of sound files
-ensure(
- 'AIRCOX_SOUND_FILE_EXT',
- ('.ogg', '.flac', '.wav', '.mp3', '.opus')
-)
+ensure("AIRCOX_SOUND_FILE_EXT", (".ogg", ".flac", ".wav", ".mp3", ".opus"))
# Tag sounds as deleted instead of deleting them when file has been removed
# from filesystem (sound monitoring)
-ensure(
- 'AIRCOX_SOUND_KEEP_DELETED',
- False
-)
+ensure("AIRCOX_SOUND_KEEP_DELETED", False)
########################################################################
# Streamer & Controllers
########################################################################
# Controllers working directory
-ensure('AIRCOX_CONTROLLERS_WORKING_DIR', '/tmp/aircox')
+ensure("AIRCOX_CONTROLLERS_WORKING_DIR", "/tmp/aircox")
########################################################################
@@ -150,12 +116,10 @@ ensure('AIRCOX_CONTROLLERS_WORKING_DIR', '/tmp/aircox')
########################################################################
# Columns for CSV file
ensure(
- 'AIRCOX_IMPORT_PLAYLIST_CSV_COLS',
- ('artist', 'title', 'minutes', 'seconds', 'tags', 'info')
+ "AIRCOX_IMPORT_PLAYLIST_CSV_COLS",
+ ("artist", "title", "minutes", "seconds", "tags", "info"),
)
# Column delimiter of csv text files
-ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')
+ensure("AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER", ";")
# Text delimiter of csv text files
-ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"')
-
-
+ensure("AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE", '"')
diff --git a/aircox/static/aircox/js/admin.js b/aircox/static/aircox/js/admin.js
index 38f5b30..93c61c8 100644
--- a/aircox/static/aircox/js/admin.js
+++ b/aircox/static/aircox/js/admin.js
@@ -34,7 +34,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
-/******/
+/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
@@ -48,20 +48,20 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ loaded: false,
/******/ exports: {}
/******/ };
-/******/
+/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
+/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
-/******/
+/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
-/******/
+/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
-/******/
+/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ !function() {
@@ -96,7 +96,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ return result;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
@@ -108,7 +108,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ return getter;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
@@ -120,7 +120,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ }
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/global */
/******/ !function() {
/******/ __webpack_require__.g = (function() {
@@ -132,12 +132,12 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ }
/******/ })();
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/make namespace object */
/******/ !function() {
/******/ // define __esModule on exports
@@ -148,7 +148,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/node module decorator */
/******/ !function() {
/******/ __webpack_require__.nmd = function(module) {
@@ -157,30 +157,30 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ return module;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ !function() {
/******/ // no baseURI
-/******/
+/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "admin": 0
/******/ };
-/******/
+/******/
/******/ // no chunk on demand loading
-/******/
+/******/
/******/ // no prefetching
-/******/
+/******/
/******/ // no preloaded
-/******/
+/******/
/******/ // no HMR
-/******/
+/******/
/******/ // no HMR manifest
-/******/
+/******/
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
-/******/
+/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
/******/ var chunkIds = data[0];
@@ -207,19 +207,19 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
-/******/
+/******/
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ }();
-/******/
+/******/
/************************************************************************/
-/******/
+/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/admin.js"); })
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
-/******/
+/******/
/******/ })()
-;
\ No newline at end of file
+;
diff --git a/aircox/static/aircox/js/chunk-common.js b/aircox/static/aircox/js/chunk-common.js
index 96b03d1..66de28d 100644
--- a/aircox/static/aircox/js/chunk-common.js
+++ b/aircox/static/aircox/js/chunk-common.js
@@ -819,4 +819,4 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
/***/ })
-}]);
\ No newline at end of file
+}]);
diff --git a/aircox/static/aircox/js/chunk-vendors.js b/aircox/static/aircox/js/chunk-vendors.js
index 4614bf5..4dddfab 100644
--- a/aircox/static/aircox/js/chunk-vendors.js
+++ b/aircox/static/aircox/js/chunk-vendors.js
@@ -842,4 +842,4 @@ eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extr
/***/ })
-}]);
\ No newline at end of file
+}]);
diff --git a/aircox/static/aircox/js/core.js b/aircox/static/aircox/js/core.js
index 4ac812e..54e818e 100644
--- a/aircox/static/aircox/js/core.js
+++ b/aircox/static/aircox/js/core.js
@@ -24,7 +24,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
-/******/
+/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
@@ -38,20 +38,20 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ loaded: false,
/******/ exports: {}
/******/ };
-/******/
+/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
+/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
-/******/
+/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
-/******/
+/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
-/******/
+/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ !function() {
@@ -86,7 +86,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ return result;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
@@ -98,7 +98,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ return getter;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
@@ -110,7 +110,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ }
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/global */
/******/ !function() {
/******/ __webpack_require__.g = (function() {
@@ -122,12 +122,12 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ }
/******/ })();
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/make namespace object */
/******/ !function() {
/******/ // define __esModule on exports
@@ -138,7 +138,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/node module decorator */
/******/ !function() {
/******/ __webpack_require__.nmd = function(module) {
@@ -147,30 +147,30 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ return module;
/******/ };
/******/ }();
-/******/
+/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ !function() {
/******/ // no baseURI
-/******/
+/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "core": 0
/******/ };
-/******/
+/******/
/******/ // no chunk on demand loading
-/******/
+/******/
/******/ // no prefetching
-/******/
+/******/
/******/ // no preloaded
-/******/
+/******/
/******/ // no HMR
-/******/
+/******/
/******/ // no HMR manifest
-/******/
+/******/
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
-/******/
+/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
/******/ var chunkIds = data[0];
@@ -197,19 +197,19 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
-/******/
+/******/
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ }();
-/******/
+/******/
/************************************************************************/
-/******/
+/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/core.js"); })
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
-/******/
+/******/
/******/ })()
-;
\ No newline at end of file
+;
diff --git a/aircox/templates/admin/aircox/filters/datetime_filter.html b/aircox/templates/admin/aircox/filters/datetime_filter.html
index c96db85..5d77845 100644
--- a/aircox/templates/admin/aircox/filters/datetime_filter.html
+++ b/aircox/templates/admin/aircox/filters/datetime_filter.html
@@ -4,4 +4,3 @@
{% endwith %}
{% endblock %}
-
diff --git a/aircox/templates/admin/aircox/filters/filter.html b/aircox/templates/admin/aircox/filters/filter.html
index a630813..26b0bd7 100644
--- a/aircox/templates/admin/aircox/filters/filter.html
+++ b/aircox/templates/admin/aircox/filters/filter.html
@@ -1,4 +1,3 @@
{% load i18n %}
{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
{% block content %}{% endblock %}
-
diff --git a/aircox/templates/admin/aircox/page_change_form.html b/aircox/templates/admin/aircox/page_change_form.html
index ca5cd81..74e5f2a 100644
--- a/aircox/templates/admin/aircox/page_change_form.html
+++ b/aircox/templates/admin/aircox/page_change_form.html
@@ -42,4 +42,3 @@
{% endif %}
{% endblock %}
-
diff --git a/aircox/templates/admin/aircox/page_change_list.html b/aircox/templates/admin/aircox/page_change_list.html
index 53cb4d8..decc6ae 100644
--- a/aircox/templates/admin/aircox/page_change_list.html
+++ b/aircox/templates/admin/aircox/page_change_list.html
@@ -17,5 +17,3 @@
{% endblock %}
{% endif %}
-
-
diff --git a/aircox/templates/admin/aircox/statistics.html b/aircox/templates/admin/aircox/statistics.html
index d7f47f3..bdcdbc7 100644
--- a/aircox/templates/admin/aircox/statistics.html
+++ b/aircox/templates/admin/aircox/statistics.html
@@ -80,5 +80,3 @@
{% endblock %}
-
-
diff --git a/aircox/templates/admin/base.html b/aircox/templates/admin/base.html
index 61e5758..4346f5e 100644
--- a/aircox/templates/admin/base.html
+++ b/aircox/templates/admin/base.html
@@ -36,7 +36,7 @@
elm.setAttribute('v-pre', true)
}
}
-
+
window.addEventListener('load', function() {
{% block init-scripts %}
aircox.init(null, {
diff --git a/aircox/templates/admin/base_site.html b/aircox/templates/admin/base_site.html
index 76456fe..95cfda5 100644
--- a/aircox/templates/admin/base_site.html
+++ b/aircox/templates/admin/base_site.html
@@ -6,5 +6,3 @@
{% endblock %}
-
-
diff --git a/aircox/templates/admin/change_form.html b/aircox/templates/admin/change_form.html
index 89a3627..c48ca37 100644
--- a/aircox/templates/admin/change_form.html
+++ b/aircox/templates/admin/change_form.html
@@ -3,4 +3,3 @@
{% block content %}
{{ block.super }}
{% endblock %}
-
diff --git a/aircox/templates/admin/index.html b/aircox/templates/admin/index.html
index 9c3571e..bcf70ae 100644
--- a/aircox/templates/admin/index.html
+++ b/aircox/templates/admin/index.html
@@ -92,4 +92,3 @@
{% endblock %}
-
diff --git a/aircox/templates/aircox/article_detail.html b/aircox/templates/aircox/article_detail.html
index 45bef35..5409d07 100644
--- a/aircox/templates/aircox/article_detail.html
+++ b/aircox/templates/aircox/article_detail.html
@@ -28,4 +28,3 @@
{% endif %}
{% endblock %}
-
diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html
index 91b17ee..50d30a5 100644
--- a/aircox/templates/aircox/base.html
+++ b/aircox/templates/aircox/base.html
@@ -164,5 +164,3 @@ Usefull context:
{% include "aircox/widgets/player.html" %}