merge diffusions and episode, work on different fixes, duration are timefield, make it work

This commit is contained in:
bkfox 2015-11-22 23:24:19 +01:00
parent 44fc4dae31
commit 25e3d4cb53
10 changed files with 357 additions and 189 deletions

View File

@ -3,15 +3,84 @@ Control Liquidsoap
""" """
import os import os
import re import re
import datetime
import collections
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.views.generic.base import View from django.utils import timezone as tz
from django.template.loader import render_to_string
import aircox_liquidsoap.settings as settings import aircox_liquidsoap.settings as settings
import aircox_liquidsoap.utils as utils import aircox_liquidsoap.utils as utils
import aircox_programs.models as models
class DiffusionInfo:
date = None
original = None
sounds = None
duration = 0
def __init__ (self, diffusion):
episode = diffusion.episode
self.original = diffusion
self.sounds = [ sound for sound in episode.sounds
if sound.type = models.Sound.Type['archive'] ]
self.sounds.sort(key = 'path')
self.date = diffusion.date
self.duration = episode.get_duration()
self.end = self.date + tz.datetime.timedelta(seconds = self.duration)
def __eq___ (self, info):
return self.original.id == info.original.id
class ControllerMonitor:
current = None
queue = None
def get_next (self, controller):
upcoming = models.Diffusion.get_next(
station = controller.station,
# diffusion__episode__not blank
# diffusion__episode__sounds not blank
)
return Monitor.Info(upcoming[0]) if upcoming else None
def playlist (self, controller):
dealer = controller.dealer
on_air = dealer.current_sound
playlist = dealer.playlist
next = self.queue[0]
# last track: time to reload playlist
if on_air == playlist[-1] or on_air not in playlist:
dealer.playlist = [sound.path for sound in next.sounds]
dealer.on = False
def current (self, controller):
# time to switch...
if on_air not in self.current.sounds:
self.current = self.queue.popleft()
if self.current.date <= tz.datetime.now() and not dealer.on:
dealer.on = True
print('start ', self.current.original)
# HERE
upcoming = self.get_next(controller)
if upcoming.date <= tz.datetime.now() and not self.current:
self.current = upcoming
if not self.upcoming or upcoming != self.upcoming:
dealer.playlist = [sound.path for sound in upcomming.sounds]
dealer.on = False
self.upcoming = upcoming
class Command (BaseCommand): class Command (BaseCommand):
@ -24,8 +93,29 @@ class Command (BaseCommand):
'-o', '--on_air', action='store_true', '-o', '--on_air', action='store_true',
help='Print what is on air' help='Print what is on air'
) )
parser.add_argument(
'-m', '--monitor', action='store_true',
help='Runs in monitor mode'
)
parser.add_argument(
'-s', '--sleep', type=int,
default=1,
help='Time to sleep before update'
)
# start and run liquidsoap
def handle (self, *args, **options): def handle (self, *args, **options):
controller = utils.Controller() connector = utils.Connector()
controller.get() self.monitor = utils.Monitor()
self.monitor.update()
if options.get('on_air'):
for id, controller in self.monitor.controller.items():
print(id, controller.master.current_sound())
if options.get('monitor'):
sleep =

View File

@ -16,7 +16,6 @@ import aircox_programs.settings as programs_settings
import aircox_programs.models as models import aircox_programs.models as models
class Command (BaseCommand): class Command (BaseCommand):
help= __doc__ help= __doc__
output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA

View File

@ -5,6 +5,7 @@ import json
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from aircox_programs.utils import to_timedelta
import aircox_programs.models as models import aircox_programs.models as models
import aircox_liquidsoap.settings as settings import aircox_liquidsoap.settings as settings
@ -25,6 +26,7 @@ class Connector:
return self.__available return self.__available
def __init__ (self, address = None): def __init__ (self, address = None):
if address:
self.address = address self.address = address
def open (self): def open (self):
@ -145,7 +147,8 @@ class Source:
@property @property
def playlist (self): def playlist (self):
""" """
The playlist as an array Get or set the playlist as an array, and update it into
the corresponding file.
""" """
try: try:
with open(self.path, 'r') as file: with open(self.path, 'r') as file:
@ -159,6 +162,12 @@ class Source:
file.write('\n'.join(sounds)) file.write('\n'.join(sounds))
self.connector.send(self.name, '_playlist.reload') self.connector.send(self.name, '_playlist.reload')
@property
def current_sound (self):
self.update()
self.metadata['initial_uri']
def stream_info (self): def stream_info (self):
""" """
Return a dict with info related to the program's stream Return a dict with info related to the program's stream
@ -221,12 +230,18 @@ class Dealer (Source):
diffusions = models.Diffusion.get_next(self.station) diffusions = models.Diffusion.get_next(self.station)
if not diffusions.count(): if not diffusions.count():
return return
diffusion = diffusions[0] diffusion = diffusions[0]
return diffusion return diffusion
def on_air (self, value = True): @property
pass def on (self):
r = self.connector.send('var.get ', self.id, '_on')
return (r == 'true')
@on.setter
def on (self, value):
return self.connector.send('var.set ', self.id, '_on',
'=', 'true' if value else 'false')
@property @property
def playlist (self): def playlist (self):
@ -242,6 +257,46 @@ class Dealer (Source):
file.write('\n'.join(sounds)) file.write('\n'.join(sounds))
def __get_queue (self, date):
"""
Return a list of diffusion candidates of being running right now.
Add an attribute "sounds" with the episode's archives.
"""
r = [ models.Diffusion.get_prev(self.station, date),
models.Diffusion.get_next(self.station, date) ]
r = [ diffusion.prefetch_related('episode__sounds')[0]
for diffusion in r if diffusion.count() ]
for diffusion in r:
setattr(diffusion, 'sounds',
[ sound.path for sound in diffusion.get_sounds() ])
return r
def __what_now (self, date, on_air, queue):
"""
Return which diffusion is on_air from the given queue
"""
for diffusion in queue:
duration = diffusion.archives_duration()
end_at = diffusion.date + tz.timedelta(seconds = diffusion.archives_duration())
if end_at < date:
continue
if diffusion.sounds and on_air in diffusion.sounds:
return diffusion
def monitor (self):
"""
Monitor playlist (if it is time to load) and if it time to trigger
the button to start a diffusion.
"""
on_air = self.current_soudn
playlist = self.playlist
queue = self.__get_queue()
current_diffusion = self.__what_now()
class Controller: class Controller:
connector = None connector = None
station = None # the related station station = None # the related station

View File

@ -28,12 +28,6 @@ class StreamInline (admin.TabularInline):
extra = 1 extra = 1
class DiffusionInline (admin.TabularInline):
model = Diffusion
fields = ('episode', 'type', 'date')
extra = 1
class TrackInline (SortableTabularInline): class TrackInline (SortableTabularInline):
fields = ['artist', 'name', 'tags', 'position'] fields = ['artist', 'name', 'tags', 'position']
form = TrackForm form = TrackForm
@ -53,10 +47,10 @@ class NameableAdmin (admin.ModelAdmin):
@admin.register(Sound) @admin.register(Sound)
class SoundAdmin (NameableAdmin): class SoundAdmin (NameableAdmin):
fields = None fields = None
list_display = ['id', 'name', 'duration', 'type', 'date', 'good_quality', 'removed', 'public'] list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed', 'public']
fieldsets = [ fieldsets = [
(None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ), (None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ),
(None, { 'fields': ['embed', 'duration', 'date'] }), (None, { 'fields': ['embed', 'duration', 'mtime'] }),
(None, { 'fields': ['removed', 'good_quality', 'public' ] } ) (None, { 'fields': ['removed', 'good_quality', 'public' ] } )
] ]
@ -73,34 +67,25 @@ class StationAdmin (NameableAdmin):
@admin.register(Program) @admin.register(Program)
class ProgramAdmin (NameableAdmin): class ProgramAdmin (NameableAdmin):
fields = NameableAdmin.fields + [ 'station', 'active' ] fields = NameableAdmin.fields + [ 'station', 'active' ]
# TODO list_display
inlines = [ ScheduleInline, StreamInline ] inlines = [ ScheduleInline, StreamInline ]
def get_form (self, request, obj=None, **kwargs): # SO#8074161
if obj and Stream.objects.filter(program = obj).count() \ #def get_form (self, request, obj=None, **kwargs):
and ScheduleInline in self.inlines: #if obj:
self.inlines.remove(ScheduleInline) # if Schedule.objects.filter(program = obj).count():
elif obj and Schedule.objects.filter(program = obj).count() \ # self.inlines.remove(StreamInline)
and StreamInline in self.inlines: # elif Stream.objects.filter(program = obj).count():
self.inlines.remove(StreamInline) # self.inlines.remove(ScheduleInline)
return super().get_form(request, obj, **kwargs) #return super().get_form(request, obj, **kwargs)
@admin.register(Episode)
class EpisodeAdmin (NameableAdmin):
list_filter = ['program'] + NameableAdmin.list_filter
fields = NameableAdmin.fields + ['sounds', 'program']
inlines = (TrackInline, DiffusionInline)
@admin.register(Diffusion) @admin.register(Diffusion)
class DiffusionAdmin (admin.ModelAdmin): class DiffusionAdmin (admin.ModelAdmin):
def archives (self, obj): def archives (self, obj):
sounds = obj.episode and \ sounds = obj.get_archives()
(os.path.basename(sound.path) for sound in obj.episode.sounds.all()
if sound.type == Sound.Type['archive'] )
return ', '.join(sounds) if sounds else '' return ', '.join(sounds) if sounds else ''
list_display = ('id', 'type', 'date', 'archives', 'episode', 'program', 'rerun') list_display = ('id', 'type', 'date', 'archives', 'program', 'initial')
list_filter = ('type', 'date', 'program') list_filter = ('type', 'date', 'program')
list_editable = ('type', 'date') list_editable = ('type', 'date')

View File

@ -25,7 +25,7 @@ class Actions:
def update (date): def update (date):
count = 0 count = 0
for schedule in Schedule.objects.filter(program__active = True) \ for schedule in Schedule.objects.filter(program__active = True) \
.order_by('rerun'): .order_by('initial'):
# in order to allow rerun links between diffusions, we save items # in order to allow rerun links between diffusions, we save items
# by schedule; # by schedule;
items = schedule.diffusions_of_month(date, exclude_saved = True) items = schedule.diffusions_of_month(date, exclude_saved = True)

View File

@ -28,6 +28,7 @@ from django.core.management.base import BaseCommand, CommandError
from aircox_programs.models import * from aircox_programs.models import *
import aircox_programs.settings as settings import aircox_programs.settings as settings
import aircox_programs.utils as utils
class Command (BaseCommand): class Command (BaseCommand):
@ -85,10 +86,10 @@ class Command (BaseCommand):
r['path'] = path r['path'] = path
return r return r
def find_episode (self, program, sound_info): def find_initial (self, program, sound_info):
""" """
For a given program, and sound path check if there is an episode to For a given program, and sound path check if there is an initial
associate to, using the diffusion's date. diffusion to associate to, using the diffusion's date.
If there is no matching episode, return None. If there is no matching episode, return None.
""" """
@ -101,10 +102,10 @@ class Command (BaseCommand):
) )
if not diffusion.count(): if not diffusion.count():
self.report(program, path, 'no diffusion found for the given date') self.report(program, sound_info['path'],
'no diffusion found for the given date')
return return
diffusion = diffusion[0] return diffusion[0]
return diffusion.episode or None
@staticmethod @staticmethod
def check_sounds (qs): def check_sounds (qs):
@ -118,7 +119,7 @@ class Command (BaseCommand):
programs = Program.objects.filter() programs = Program.objects.filter()
for program in programs: for program in programs:
print('- program ', program.name) print('- program', program.name)
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'], type = Sound.Type['archive'],
@ -153,18 +154,22 @@ class Command (BaseCommand):
sound.__dict__.update(sound_kwargs) sound.__dict__.update(sound_kwargs)
sound.save(check = False) sound.save(check = False)
# episode and relation # initial diffusion association
if 'year' in sound_info: if 'year' in sound_info:
episode = self.find_episode(program, sound_info) initial = self.find_initial(program, sound_info)
if episode: if initial:
for sound_ in episode.sounds.get_queryset(): if initial.initial:
if sound_.path == sound.path: # FIXME: allow user to overwrite rerun info?
break self.report(program, path,
'the diffusion must be an initial diffusion')
else: else:
self.report(program, path, 'add sound to episode ', sound = initial.sounds.get_queryset() \
episode.id) .filter(path == sound.path)
episode.sounds.add(sound) if not sound:
episode.save() self.report(program, path,
'add sound to diffusion ', initial.id)
initial.sounds.add(sound)
initial.save()
self.check_sounds(Sound.objects.filter(path__startswith = subdir)) self.check_sounds(Sound.objects.filter(path__startswith = subdir))
@ -191,7 +196,8 @@ class Command (BaseCommand):
def update_stats(sound_info, sound): def update_stats(sound_info, sound):
stats = sound_info.get_file_stats() stats = sound_info.get_file_stats()
if stats: if stats:
sound.duration = int(stats.get('length')) duration = int(stats.get('length'))
sound.duration = utils.seconds_to_time(duration)
for sound_info in cmd.good: for sound_info in cmd.good:
sound = Sound.objects.get(path = sound_info.path) sound = Sound.objects.get(path = sound_info.path)

View File

@ -48,14 +48,14 @@ class Nameable (models.Model):
class Track (Nameable): class Track (Nameable):
""" """
Track of a playlist of an episode. The position can either be expressed Track of a playlist of a diffusion. The position can either be expressed
as the position in the playlist or as the moment in seconds it started. as the position in the playlist or as the moment in seconds it started.
""" """
# There are no nice solution for M2M relations ship (even without # There are no nice solution for M2M relations ship (even without
# through) in django-admin. So we unfortunately need to make one- # through) in django-admin. So we unfortunately need to make one-
# to-one relations and add a position argument # to-one relations and add a position argument
episode = models.ForeignKey( diffusion = models.ForeignKey(
'Episode', 'Diffusion',
) )
artist = models.CharField( artist = models.CharField(
_('artist'), _('artist'),
@ -83,7 +83,7 @@ class Track (Nameable):
class Sound (Nameable): class Sound (Nameable):
""" """
A Sound is the representation of a sound file that can be either an excerpt A Sound is the representation of a sound file that can be either an excerpt
or a complete archive of the related episode. or a complete archive of the related diffusion.
The podcasting and public access permissions of a Sound are managed through The podcasting and public access permissions of a Sound are managed through
the related program info. the related program info.
@ -114,13 +114,13 @@ class Sound (Nameable):
blank = True, null = True, blank = True, null = True,
help_text = _('HTML code used to embed a sound from external plateform'), help_text = _('HTML code used to embed a sound from external plateform'),
) )
duration = models.IntegerField( duration = models.TimeField(
_('duration'), _('duration'),
blank = True, null = True, blank = True, null = True,
help_text = _('duration in seconds'), help_text = _('duration of the sound'),
) )
date = models.DateTimeField( mtime = models.DateTimeField(
_('date'), _('modification time'),
blank = True, null = True, blank = True, null = True,
help_text = _('last modification date and time'), help_text = _('last modification date and time'),
) )
@ -151,6 +151,9 @@ class Sound (Nameable):
return tz.make_aware(mtime, tz.get_current_timezone()) return tz.make_aware(mtime, tz.get_current_timezone())
def file_exists (self): def file_exists (self):
"""
Return true if the file still exists
"""
return os.path.exists(self.path) return os.path.exists(self.path)
def check_on_file (self): def check_on_file (self):
@ -168,8 +171,8 @@ class Sound (Nameable):
self.removed = False self.removed = False
mtime = self.get_mtime() mtime = self.get_mtime()
if self.date != mtime: if self.mtime != mtime:
self.date = mtime self.mtime = mtime
self.good_quality = False self.good_quality = False
return True return True
return old_removed != self.removed return old_removed != self.removed
@ -226,8 +229,8 @@ class Stream (models.Model):
class Schedule (models.Model): class Schedule (models.Model):
""" """
A Schedule defines time slots of programs' diffusions. It can be a run or A Schedule defines time slots of programs' diffusions. It can be an initial
a rerun (in such case it is linked to the related schedule). 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 # Frequency for schedules. Basically, it is a mask of bits where each bit is
# a week. Bits > rank 5 are used for special schedules. # a week. Bits > rank 5 are used for special schedules.
@ -255,16 +258,17 @@ class Schedule (models.Model):
date = models.DateTimeField(_('date')) date = models.DateTimeField(_('date'))
duration = models.TimeField( duration = models.TimeField(
_('duration'), _('duration'),
help_text = _('regular duration'),
) )
frequency = models.SmallIntegerField( frequency = models.SmallIntegerField(
_('frequency'), _('frequency'),
choices = VerboseFrequency.items(), choices = VerboseFrequency.items(),
) )
rerun = models.ForeignKey( initial = models.ForeignKey(
'self', 'self',
verbose_name = _('rerun'), verbose_name = _('initial'),
blank = True, null = True, blank = True, null = True,
help_text = "Schedule of a rerun of this one", help_text = 'this schedule is a rerun of this one',
) )
def match (self, date = None, check_time = True): def match (self, date = None, check_time = True):
@ -333,7 +337,6 @@ class Schedule (models.Model):
fweek = 0 fweek = 0
week = self.date.isocalendar()[1] week = self.date.isocalendar()[1]
weeks = 0b010101 if not (fweek + week) % 2 else 0b001010 weeks = 0b010101 if not (fweek + week) % 2 else 0b001010
print(date, fweek, week, "{0:b}".format(weeks))
dates = [] dates = []
for week in range(0,5): for week in range(0,5):
@ -341,10 +344,8 @@ class Schedule (models.Model):
if not weeks & (0b1 << week): if not weeks & (0b1 << week):
continue continue
wdate = date + tz.timedelta(days = week * 7) wdate = date + tz.timedelta(days = week * 7)
print(wdate, wdate.month == date.month)
if wdate.month == date.month: if wdate.month == date.month:
dates.append(self.normalize(wdate)) dates.append(self.normalize(wdate))
print(dates)
return dates return dates
def diffusions_of_month (self, date, exclude_saved = False): def diffusions_of_month (self, date, exclude_saved = False):
@ -353,9 +354,6 @@ class Schedule (models.Model):
can be not in the database. can be not in the database.
If exclude_saved, exclude all diffusions that are yet in the database. If exclude_saved, exclude all diffusions that are yet in the database.
When a Diffusion is created, it tries to attach the corresponding
episode using a match of episode.date (and takes care of rerun case);
""" """
dates = self.dates_of_month(date) dates = self.dates_of_month(date)
saved = Diffusion.objects.filter(date__in = dates, saved = Diffusion.objects.filter(date__in = dates,
@ -372,21 +370,19 @@ class Schedule (models.Model):
# others # others
for date in dates: for date in dates:
first_date = date first_date = date
if self.rerun: if self.initial:
first_date -= self.date - self.rerun.date first_date -= self.date - self.initial.date
first_diffusion = Diffusion.objects.filter(date = first_date, first_diffusion = Diffusion.objects.filter(date = first_date,
program = self.program) program = self.program)
first_diffusion = first_diffusion[0] if first_diffusion.count() \ first_diffusion = first_diffusion[0] if first_diffusion.count() \
else None else None
episode = first_diffusion.episode if first_diffusion else None
# print(self.rerun, episode, first_diffusion, first_date)
diffusions.append(Diffusion( diffusions.append(Diffusion(
episode = episode,
program = self.program, program = self.program,
type = Diffusion.Type['unconfirmed'], type = Diffusion.Type['unconfirmed'],
initial = first_diffusion if self.initial else None,
date = date, date = date,
rerun = first_diffusion if self.rerun else None duration = self.duration,
)) ))
return diffusions return diffusions
@ -400,93 +396,30 @@ class Schedule (models.Model):
verbose_name_plural = _('Schedules') verbose_name_plural = _('Schedules')
class Diffusion (models.Model): class Log (models.Model):
""" """
A Diffusion is a cell in the timetable that is linked to an episode. A Log a played sound start and stop, or a single message
diffusion can have different status that tells us what happens / did
happened or not.
A Diffusion can have different types:
- default: simple diffusion that is planified / did occurred
- unconfirmed: a generated diffusion that has not been confirmed and thus
is not yet planified
- cancel: the diffusion has been canceled
- stop: the diffusion has been manually stopped
""" """
Type = { sound = models.ForeignKey(
'default': 0x00, # simple diffusion (done/planed) 'Sound',
'unconfirmed': 0x01, # scheduled by the generator but not confirmed for diffusion help_text = 'Played sound',
'cancel': 0x02, # cancellation happened; used to inform users
# 'restart': 0x03, # manual restart; used to remix/give up antenna
'stop': 0x04, # diffusion has been forced to stop
}
for key, value in Type.items():
ugettext_lazy(key)
episode = models.ForeignKey (
'Episode',
blank = True, null = True, blank = True, null = True,
verbose_name = _('episode'),
) )
program = models.ForeignKey ( stream = models.ForeignKey(
'Program', 'Stream',
verbose_name = _('program'), blank = True, null = True,
) )
type = models.SmallIntegerField( start = models.DateTimeField(
verbose_name = _('type'), 'start',
choices = [ (y, x) for x,y in Type.items() ], )
) stop = models.DateTimeField(
date = models.DateTimeField( _('start of the diffusion') ) 'stop',
rerun = models.ForeignKey ( blank = True, null = True,
'self', )
verbose_name = _('rerun'), comment = models.CharField(
max_length = 512,
blank = True, null = True, blank = True, null = True,
help_text = _('the diffusion is a rerun of this one. Remove this if '
'you want to change the concerned episode')
) )
@classmethod
def get_next (cl, station = None):
"""
Return a queryset with the upcoming diffusions, ordered by
+date
"""
args = {
'date__gte': tz.datetime.now()
}
if station:
args['program__station'] = station
return cl.objects.filter(**args).order_by('date')
@classmethod
def get_prev (cl, station = None):
"""
Return a queryset with the previous diffusion, ordered by
-date
"""
args = {
'date__lt': tz.datetime.now()
}
if station:
args['program__station'] = station
return cl.objects.filter(**args).order_by('-date')
def save (self, *args, **kwargs):
if self.rerun:
self.episode = self.rerun.episode
self.program = self.episode.program
elif self.episode:
self.program = self.episode.program
super(Diffusion, self).save(*args, **kwargs)
def __str__ (self):
return self.program.name + ' on ' + str(self.date) \
+ str(self.type)
class Meta:
verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions')
class Station (Nameable): class Station (Nameable):
@ -570,25 +503,114 @@ class Program (Nameable):
if schedule.match(date, check_time = False): if schedule.match(date, check_time = False):
return schedule return schedule
class Episode (Nameable):
class Diffusion (models.Model):
""" """
Occurrence of a program, can have multiple sounds (archive/excerpt) and A Diffusion is an occurrence of a Program that is scheduled on the
a playlist (with assigned tracks) station's timetable. It can be a rerun of a previous diffusion. In such
a case, use rerun's info instead of its own.
A Diffusion without any rerun is named Episode (previously, a
Diffusion was different from an Episode, but in the end, an
episode only has a name, a linked program, and a list of sounds, so we
finally merge theme).
A Diffusion can have different types:
- default: simple diffusion that is planified / did occurred
- unconfirmed: a generated diffusion that has not been confirmed and thus
is not yet planified
- cancel: the diffusion has been canceled
- stop: the diffusion has been manually stopped
""" """
program = models.ForeignKey( Type = {
Program, 'default': 0x00, # confirmed diffusion case FIXME
'unconfirmed': 0x01, # scheduled by the generator but not confirmed for diffusion
'cancel': 0x02, # cancellation happened; used to inform users
# 'restart': 0x03, # manual restart; used to remix/give up antenna
}
for key, value in Type.items():
ugettext_lazy(key)
# common
program = models.ForeignKey (
'Program',
verbose_name = _('program'), verbose_name = _('program'),
help_text = _('parent program'),
blank = True, null = True,
) )
sounds = models.ManyToManyField( sounds = models.ManyToManyField(
Sound, Sound,
blank = True, blank = True,
verbose_name = _('sounds'), verbose_name = _('sounds'),
) )
# specific
type = models.SmallIntegerField(
verbose_name = _('type'),
choices = [ (y, x) for x,y in Type.items() ],
)
initial = models.ForeignKey (
'self',
verbose_name = _('initial'),
blank = True, null = True,
help_text = _('the diffusion is a rerun of this one')
)
date = models.DateTimeField( _('start of the diffusion') )
duration = models.TimeField(
_('duration'),
blank = True, null = True,
help_text = _('regular duration'),
)
def archives_duration (self):
"""
Get total duration of the archives. May differ from the schedule
duration.
"""
return sum([ sound.duration for sound in self.sounds
if sound.type == Sound.Type['archive']])
def get_archives (self):
"""
Return an ordered list of archives sounds for the given episode.
"""
r = [ sound for sound in self.sounds.all()
if sound.type == Sound.Type['archive'] ]
r.sort(key = 'path')
return r
@classmethod
def get_next (cl, station = None, date = None, **filter_args):
"""
Return a queryset with the upcoming diffusions, ordered by
+date
"""
filter_args['date__gte'] = date_or_default(date)
if station:
filter_args['program__station'] = station
return cl.objects.filter(**filter_args).order_by('date')
@classmethod
def get_prev (cl, station = None, date = None, **filter_args):
"""
Return a queryset with the previous diffusion, ordered by
-date
"""
filter_args['date__lte'] = date_or_default(date)
if station:
filter_args['program__station'] = station
return cl.objects.filter(**filter_args).order_by('-date')
def save (self, *args, **kwargs):
if self.initial:
if self.initial.initial:
self.initial = self.initial.initial
self.program = self.initial.program
super(Diffusion, self).save(*args, **kwargs)
def __str__ (self):
return self.program.name + ' on ' + str(self.date) \
+ str(self.type)
class Meta: class Meta:
verbose_name = _('Episode') verbose_name = _('Diffusion')
verbose_name_plural = _('Episodes') verbose_name_plural = _('Diffusions')

View File

@ -1,6 +1,23 @@
import datetime
def to_timedelta (time):
"""
Transform a datetime or a time instance to a timedelta,
only using time info
"""
return datetime.timedelta(
hours = time.hour,
minutes = time.minute,
seconds = time.seconds
)
def seconds_to_time (seconds):
"""
Seconds to datetime.time
"""
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return datetime.time(hour = hours, minute = minutes, second = seconds)
def ensure_list (value):
if type(value) in (list, set, tuple):
return value
return [value]

View File

@ -38,15 +38,9 @@ def add_inline (base_model, post_model, prepend = False):
add_inline(programs.Program, Program, True) add_inline(programs.Program, Program, True)
add_inline(programs.Episode, Episode, True) # add_inline(programs.Episode, Episode, True)
admin.site.register(Program) admin.site.register(Program)
admin.site.register(Episode) # admin.site.register(Episode)
#class ArticleAdmin (DescriptionAdmin):
# fieldsets = copy.deepcopy(DescriptionAdmin.fieldsets)
#
# fieldsets[1][1]['fields'] += ['static_page']

View File

@ -14,11 +14,11 @@ class Program (RelatedPost):
class Episode (RelatedPost): class Episode (RelatedPost):
class Relation: class Relation:
model = programs.Episode model = programs.Diffusion
bind_mapping = True bind_mapping = True
mapping = { mapping = {
'thread': 'program', 'thread': 'program',
'title': 'name', # 'title': 'name',
'content': 'description', # 'content': 'description',
} }