diff --git a/aircox/admin.py b/aircox/admin.py
deleted file mode 100755
index 9aefdd8..0000000
--- a/aircox/admin.py
+++ /dev/null
@@ -1,208 +0,0 @@
-import copy
-
-from django import forms
-from django.contrib import admin
-from django.contrib.contenttypes.admin import GenericTabularInline
-from django.db import models
-from django.utils.translation import ugettext as _, ugettext_lazy
-
-from aircox.models import *
-
-#
-# Inlines
-#
-class SoundInline(admin.TabularInline):
- model = Sound
-
-
-class ScheduleInline(admin.TabularInline):
- model = Schedule
- extra = 1
-
-class StreamInline(admin.TabularInline):
- fields = ['delay', 'begin', 'end']
- model = Stream
- extra = 1
-
-class SoundInline(admin.TabularInline):
- fields = ['type', 'path', 'duration','public']
- # readonly_fields = fields
- model = Sound
- extra = 0
-
-
-class DiffusionInline(admin.StackedInline):
- model = Diffusion
- extra = 0
- fields = ['type', 'start', 'end']
-
-class NameableAdmin(admin.ModelAdmin):
- fields = [ 'name' ]
-
- list_display = ['id', 'name']
- list_filter = []
- search_fields = ['name',]
-
-
-class TrackInline(GenericTabularInline):
- ct_field = 'related_type'
- ct_fk_field = 'related_id'
- model = Track
- extra = 0
- fields = ('artist', 'title', 'info', 'position', 'in_seconds', 'tags')
-
- list_display = ['artist','title','tags','related']
- list_filter = ['artist','title','tags']
-
-
-@admin.register(Sound)
-class SoundAdmin(NameableAdmin):
- fields = None
- list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime',
- 'public', 'good_quality', 'path']
- list_filter = ('program', 'type', 'good_quality', 'public')
- fieldsets = [
- (None, { 'fields': NameableAdmin.fields +
- ['path', 'type', 'program', 'diffusion'] } ),
- (None, { 'fields': ['embed', 'duration', 'public', 'mtime'] }),
- (None, { 'fields': ['good_quality' ] } )
- ]
- readonly_fields = ('path', 'duration',)
- inlines = [TrackInline]
-
-
-@admin.register(Stream)
-class StreamAdmin(admin.ModelAdmin):
- list_display = ('id', 'program', 'delay', 'begin', 'end')
-
-
-@admin.register(Program)
-class ProgramAdmin(NameableAdmin):
- def schedule(self, obj):
- return Schedule.objects.filter(program = obj).count() > 0
- schedule.boolean = True
- schedule.short_description = _("Schedule")
-
- list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station')
- fields = NameableAdmin.fields + [ 'active', 'station','sync' ]
- inlines = [ ScheduleInline, StreamInline ]
-
- # SO#8074161
- #def get_form(self, request, obj=None, **kwargs):
- #if obj:
- # if Schedule.objects.filter(program = obj).count():
- # self.inlines.remove(StreamInline)
- # elif Stream.objects.filter(program = obj).count():
- # self.inlines.remove(ScheduleInline)
- #return super().get_form(request, obj, **kwargs)
-
-
-@admin.register(Diffusion)
-class DiffusionAdmin(admin.ModelAdmin):
- def archives(self, obj):
- sounds = [ str(s) for s in obj.get_sounds(archive=True)]
- return ', '.join(sounds) if sounds else ''
-
- def conflicts_(self, obj):
- if obj.conflicts.count():
- return obj.conflicts.count()
- return ''
-
- def start_date(self, obj):
- return obj.local_date.strftime('%Y/%m/%d %H:%M')
- start_date.short_description = _('start')
-
- def end_date(self, obj):
- return obj.local_end.strftime('%H:%M')
- end_date.short_description = _('end')
-
- def first(self, obj):
- return obj.initial.start if obj.initial else ''
-
- list_display = ('id', 'program', 'start_date', 'end_date', 'type', 'first', 'archives', 'conflicts_')
- list_filter = ('type', 'start', 'program')
- list_editable = ('type',)
- ordering = ('-start', 'id')
-
- fields = ['type', 'start', 'end', 'initial', 'program', 'conflicts']
- readonly_fields = ('conflicts',)
- inlines = [ DiffusionInline, SoundInline ]
-
-
- def get_form(self, request, obj=None, **kwargs):
- if request.user.has_perm('aircox_program.programming'):
- self.readonly_fields = []
- else:
- self.readonly_fields = ['program', 'start', 'end']
- return super().get_form(request, obj, **kwargs)
-
- def get_object(self, *args, **kwargs):
- """
- We want rerun to redirect to the given object.
- """
- obj = super().get_object(*args, **kwargs)
- if obj and obj.initial:
- obj = obj.initial
- return obj
-
- def get_queryset(self, request):
- qs = super().get_queryset(request)
- if request.GET and len(request.GET):
- return qs
- return qs.exclude(type = Diffusion.Type.unconfirmed)
-
-
-@admin.register(Schedule)
-class ScheduleAdmin(admin.ModelAdmin):
- def program_name(self, obj):
- return obj.program.name
- program_name.short_description = _('Program')
-
- def day(self, obj):
- return '' # obj.date.strftime('%A')
- day.short_description = _('Day')
-
- def rerun(self, obj):
- return obj.initial != None
- rerun.short_description = _('Rerun')
- rerun.boolean = True
-
-
- list_filter = ['frequency', 'program']
- list_display = ['id', 'program_name', 'frequency', 'day', 'date',
- 'time', 'duration', 'timezone', 'rerun']
- list_editable = ['time', 'timezone', 'duration']
-
-
- def get_readonly_fields(self, request, obj=None):
- if obj:
- return ['program', 'date', 'frequency']
- else:
- return []
-
-
-@admin.register(Track)
-class TrackAdmin(admin.ModelAdmin):
- list_display = ['id', 'title', 'artist', 'position', 'in_seconds', 'related']
-
-
-
-# TODO: sort & redo
-class PortInline(admin.StackedInline):
- model = Port
- extra = 0
-
-@admin.register(Station)
-class StationAdmin(admin.ModelAdmin):
- inlines = [ PortInline ]
-
-@admin.register(Log)
-class LogAdmin(admin.ModelAdmin):
- list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
- list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
-
-admin.site.register(Port)
-
-
-
-
diff --git a/aircox/admin/__init__.py b/aircox/admin/__init__.py
new file mode 100644
index 0000000..4a0752a
--- /dev/null
+++ b/aircox/admin/__init__.py
@@ -0,0 +1,5 @@
+from .base import *
+from .diffusion import DiffusionAdmin
+# from .playlist import PlaylistAdmin
+from .sound import SoundAdmin
+
diff --git a/aircox/admin/base.py b/aircox/admin/base.py
new file mode 100644
index 0000000..56c7577
--- /dev/null
+++ b/aircox/admin/base.py
@@ -0,0 +1,94 @@
+from django import forms
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.translation import ugettext as _, ugettext_lazy
+from django.utils.safestring import mark_safe
+
+from adminsortable2.admin import SortableInlineAdminMixin
+
+from aircox.models import *
+
+
+class ScheduleInline(admin.TabularInline):
+ model = Schedule
+ extra = 1
+
+class StreamInline(admin.TabularInline):
+ fields = ['delay', 'begin', 'end']
+ model = Stream
+ extra = 1
+
+class NameableAdmin(admin.ModelAdmin):
+ fields = [ 'name' ]
+
+ list_display = ['id', 'name']
+ list_filter = []
+ search_fields = ['name',]
+
+
+@admin.register(Stream)
+class StreamAdmin(admin.ModelAdmin):
+ list_display = ('id', 'program', 'delay', 'begin', 'end')
+
+
+@admin.register(Program)
+class ProgramAdmin(NameableAdmin):
+ def schedule(self, obj):
+ return Schedule.objects.filter(program = obj).count() > 0
+ schedule.boolean = True
+ schedule.short_description = _("Schedule")
+
+ list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station')
+ fields = NameableAdmin.fields + [ 'active', 'station','sync' ]
+ inlines = [ ScheduleInline, StreamInline ]
+
+
+@admin.register(Schedule)
+class ScheduleAdmin(admin.ModelAdmin):
+ def program_name(self, obj):
+ return obj.program.name
+ program_name.short_description = _('Program')
+
+ def day(self, obj):
+ return '' # obj.date.strftime('%A')
+ day.short_description = _('Day')
+
+ def rerun(self, obj):
+ return obj.initial is not None
+ rerun.short_description = _('Rerun')
+ rerun.boolean = True
+
+
+ list_filter = ['frequency', 'program']
+ list_display = ['id', 'program_name', 'frequency', 'day', 'date',
+ 'time', 'duration', 'timezone', 'rerun']
+ list_editable = ['time', 'timezone', 'duration']
+
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj:
+ return ['program', 'date', 'frequency']
+ else:
+ return []
+
+# TODO: sort & redo
+class PortInline(admin.StackedInline):
+ model = Port
+ extra = 0
+
+
+@admin.register(Station)
+class StationAdmin(admin.ModelAdmin):
+ inlines = [PortInline]
+
+
+@admin.register(Log)
+class LogAdmin(admin.ModelAdmin):
+ list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
+ list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
+
+admin.site.register(Port)
+
+
+
+
diff --git a/aircox/admin/diffusion.py b/aircox/admin/diffusion.py
new file mode 100644
index 0000000..1ed5d59
--- /dev/null
+++ b/aircox/admin/diffusion.py
@@ -0,0 +1,81 @@
+from django.contrib import admin
+from django.utils.translation import ugettext as _, ugettext_lazy
+
+from aircox.models import Diffusion, Sound, Track
+
+from .playlist import TracksInline
+
+
+class SoundInline(admin.TabularInline):
+ model = Sound
+ fk_name = 'diffusion'
+ fields = ['type', 'path', 'duration','public']
+ readonly_fields = ['type']
+ extra = 0
+
+
+class RediffusionInline(admin.StackedInline):
+ model = Diffusion
+ fk_name = 'initial'
+ extra = 0
+ fields = ['type', 'start', 'end']
+
+
+@admin.register(Diffusion)
+class DiffusionAdmin(admin.ModelAdmin):
+ def archives(self, obj):
+ sounds = [str(s) for s in obj.get_sounds(archive=True)]
+ return ', '.join(sounds) if sounds else ''
+
+ def conflicts_count(self, obj):
+ if obj.conflicts.count():
+ return obj.conflicts.count()
+ return ''
+ conflicts_count.short_description = _('Conflicts')
+
+ def start_date(self, obj):
+ return obj.local_date.strftime('%Y/%m/%d %H:%M')
+ start_date.short_description = _('start')
+
+ def end_date(self, obj):
+ return obj.local_end.strftime('%H:%M')
+ end_date.short_description = _('end')
+
+ def first(self, obj):
+ return obj.initial.start if obj.initial else ''
+
+ list_display = ('id', 'program', 'start_date', 'end_date', 'type', 'first', 'archives', 'conflicts_count')
+ list_filter = ('type', 'start', 'program')
+ list_editable = ('type',)
+ ordering = ('-start', 'id')
+
+ fields = ['type', 'start', 'end', 'initial', 'program', 'conflicts']
+ readonly_fields = ('conflicts',)
+ inlines = [TracksInline, RediffusionInline, SoundInline]
+
+ def get_playlist(self, request, obj=None):
+ return obj and getattr(obj, 'playlist', None)
+
+ def get_form(self, request, obj=None, **kwargs):
+ if request.user.has_perm('aircox_program.programming'):
+ self.readonly_fields = []
+ else:
+ self.readonly_fields = ['program', 'start', 'end']
+ return super().get_form(request, obj, **kwargs)
+
+ def get_object(self, *args, **kwargs):
+ """
+ We want rerun to redirect to the given object.
+ """
+ obj = super().get_object(*args, **kwargs)
+ if obj and obj.initial:
+ obj = obj.initial
+ return obj
+
+ def get_queryset(self, request):
+ qs = super().get_queryset(request)
+ if request.GET and len(request.GET):
+ return qs
+ return qs.exclude(type=Diffusion.Type.unconfirmed)
+
+
diff --git a/aircox/admin/mixins.py b/aircox/admin/mixins.py
new file mode 100644
index 0000000..3d2ed6a
--- /dev/null
+++ b/aircox/admin/mixins.py
@@ -0,0 +1,42 @@
+class UnrelatedInlineMixin:
+ """
+ Inline class that can be included in an admin change view whose model
+ is not directly related to inline's model.
+ """
+ view_model = None
+ parent_model = None
+ parent_fk = ''
+
+ def __init__(self, parent_model, admin_site):
+ self.view_model = parent_model
+ super().__init__(self.parent_model, admin_site)
+
+ def get_parent(self, view_obj):
+ """ Get formset's instance from `obj` of AdminSite's change form. """
+ field = self.parent_model._meta.get_field(self.parent_fk).remote_field
+ return getattr(view_obj, field.name, None)
+
+ def save_parent(self, parent, view_obj):
+ """ Save formset's instance. """
+ setattr(parent, self.parent_fk, view_obj)
+ parent.save()
+ return parent
+
+ def get_formset(self, request, obj):
+ ParentFormSet = super().get_formset(request, obj)
+ inline = self
+ class FormSet(ParentFormSet):
+ view_obj = None
+
+ def __init__(self, *args, instance=None, **kwargs):
+ self.view_obj = instance
+ instance = inline.get_parent(instance)
+ self.instance = instance
+ super().__init__(*args, instance=instance, **kwargs)
+
+ def save(self):
+ inline.save_parent(self.instance, self.view_obj)
+ return super().save()
+ return FormSet
+
+
diff --git a/aircox/admin/playlist.py b/aircox/admin/playlist.py
new file mode 100644
index 0000000..d3c6ca1
--- /dev/null
+++ b/aircox/admin/playlist.py
@@ -0,0 +1,41 @@
+from django.contrib import admin
+from django.utils.translation import ugettext as _, ugettext_lazy
+
+from adminsortable2.admin import SortableInlineAdminMixin
+
+from aircox.models import Track
+
+
+class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
+ template = 'admin/aircox/playlist_inline.html'
+ model = Track
+ extra = 0
+ fields = ('position', 'artist', 'title', 'info', 'timestamp', 'tags')
+
+ list_display = ['artist', 'title', 'tags', 'related']
+ list_filter = ['artist', 'title', 'tags']
+
+
+@admin.register(Track)
+class TrackAdmin(admin.ModelAdmin):
+ # TODO: url to filter by tag
+ def tag_list(self, obj):
+ return u", ".join(o.name for o in obj.tags.all())
+
+ list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound']
+ list_editable = ['artist', 'title']
+ list_filter = ['sound', 'diffusion', 'artist', 'title', 'tags']
+ fieldsets = [
+ (_('Playlist'), {'fields': ['diffusion', 'sound', 'position', 'timestamp']}),
+ (_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
+ ]
+
+ # TODO on edit: readonly_fields = ['diffusion', 'sound']
+
+#@admin.register(Playlist)
+#class PlaylistAdmin(admin.ModelAdmin):
+# fields = ['diffusion', 'sound']
+# inlines = [TracksInline]
+# # TODO: dynamic read only fields
+
+
diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py
new file mode 100644
index 0000000..06e1a9f
--- /dev/null
+++ b/aircox/admin/sound.py
@@ -0,0 +1,24 @@
+from django.contrib import admin
+from django.utils.translation import ugettext as _, ugettext_lazy
+
+from aircox.models import Sound
+from .base import NameableAdmin
+from .playlist import TracksInline
+
+
+@admin.register(Sound)
+class SoundAdmin(NameableAdmin):
+ fields = None
+ list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime',
+ 'public', 'good_quality', 'path']
+ list_filter = ('program', 'type', 'good_quality', 'public')
+ fieldsets = [
+ (None, {'fields': NameableAdmin.fields +
+ ['path', 'type', 'program', 'diffusion']}),
+ (None, {'fields': ['embed', 'duration', 'public', 'mtime']}),
+ (None, {'fields': ['good_quality']})
+ ]
+ readonly_fields = ('path', 'duration',)
+ inlines = [TracksInline]
+
+
diff --git a/aircox/management/commands/import_playlist.py b/aircox/management/commands/import_playlist.py
index 5870455..af0cd65 100755
--- a/aircox/management/commands/import_playlist.py
+++ b/aircox/management/commands/import_playlist.py
@@ -29,62 +29,65 @@ class Importer:
path = None
data = None
tracks = None
+ track_kwargs = {}
- def __init__(self, related = None, path = None, save = False):
- if path:
- self.read(path)
- if related:
- self.make_playlist(related, save)
+ def __init__(self, path=None, **track_kwargs):
+ self.path = path
+ self.track_kwargs = track_kwargs
def reset(self):
self.data = None
self.tracks = None
- def read(self, path):
- if not os.path.exists(path):
+ def run(self):
+ self.read()
+ if self.track_kwargs.get('sound') is not None:
+ self.make_playlist()
+
+ def read(self):
+ if not os.path.exists(self.path):
return True
- with open(path, 'r') as file:
- logger.info('start reading csv ' + path)
- self.path = path
+ with open(self.path, 'r') as file:
+ logger.info('start reading csv ' + self.path)
self.data = list(csv.DictReader(
(row for row in file
if not (row.startswith('#') or row.startswith('\ufeff#'))
- and row.strip()
- ),
- fieldnames = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
- delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
- quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
+ and row.strip()),
+ fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
+ delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
+ quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
))
- def make_playlist(self, related, save = False):
+ def make_playlist(self):
"""
Make a playlist from the read data, and return it. If save is
true, save it into the database
"""
+ if self.track_kwargs.get('sound') is None:
+ logger.error('related track\'s sound is missing. Skip import of ' +
+ self.path + '.')
+ return
+
maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
tracks = []
logger.info('parse csv file ' + self.path)
- in_seconds = ('minutes' or 'seconds') in maps
+ has_timestamp = ('minutes' or 'seconds') in maps
for index, line in enumerate(self.data):
if ('title' or 'artist') not in line:
return
-
try:
- position = \
- int(line.get('minutes') or 0) * 60 + \
- int(line.get('seconds') or 0) \
- if in_seconds else index
+ timestamp = int(line.get('minutes') or 0) * 60 + \
+ int(line.get('seconds') or 0) \
+ if has_timestamp else None
track, created = Track.objects.get_or_create(
- related_type = ContentType.objects.get_for_model(related),
- related_id = related.pk,
- title = line.get('title'),
- artist = line.get('artist'),
- position = position,
+ title=line.get('title'),
+ artist=line.get('artist'),
+ position=index,
+ **self.track_kwargs
)
-
- track.in_seconds = in_seconds
+ track.timestamp = timestamp
track.info = line.get('info')
tags = line.get('tags')
if tags:
@@ -97,8 +100,7 @@ class Importer:
)
continue
- if save:
- track.save()
+ track.save()
tracks.append(track)
self.tracks = tracks
return tracks
@@ -107,10 +109,8 @@ class Importer:
class Command (BaseCommand):
help= __doc__
- def add_arguments (self, parser):
+ def add_arguments(self, parser):
parser.formatter_class=RawTextHelpFormatter
- now = tz.datetime.today()
-
parser.add_argument(
'path', metavar='PATH', type=str,
help='path of the input playlist to read'
@@ -128,27 +128,24 @@ class Command (BaseCommand):
def handle (self, path, *args, **options):
# FIXME: absolute/relative path of sounds vs given path
if options.get('sound'):
- related = Sound.objects.filter(
- path__icontains = options.get('sound')
+ sound = Sound.objects.filter(
+ path__icontains=options.get('sound')
).first()
else:
path_, ext = os.path.splitext(path)
- related = Sound.objects.filter(path__icontains = path_).first()
+ sound = Sound.objects.filter(path__icontains=path_).first()
- if not related:
+ if not sound:
logger.error('no sound found in the database for the path ' \
'{path}'.format(path=path))
return
- if options.get('diffusion') and related.diffusion:
- related = related.diffusion
+ if options.get('diffusion') and sound.diffusion:
+ sound = sound.diffusion
- importer = Importer(related = related, path = path, save = True)
+ importer = Importer(path, sound=sound).run()
for track in importer.tracks:
- logger.info('imported track at {pos}: {title}, by '
- '{artist}'.format(
- pos = track.position,
- title = track.title, artist = track.artist
- )
- )
+ logger.info('track #{pos} imported: {title}, by {artist}'.format(
+ pos=track.position, title=track.title, artist=track.artist
+ ))
diff --git a/aircox/management/commands/sounds_monitor.py b/aircox/management/commands/sounds_monitor.py
index 8bbe58c..6f4704e 100755
--- a/aircox/management/commands/sounds_monitor.py
+++ b/aircox/management/commands/sounds_monitor.py
@@ -43,6 +43,7 @@ import aircox.utils as utils
logger = logging.getLogger('aircox.tools')
+
class SoundInfo:
name = ''
sound = None
@@ -76,7 +77,7 @@ class SoundInfo:
file_name)
if not (r and r.groupdict()):
- r = { 'name': file_name }
+ r = {'name': file_name}
logger.info('file name can not be parsed -> %s', value)
else:
r = r.groupdict()
@@ -93,7 +94,7 @@ class SoundInfo:
self.n = r.get('n')
return r
- def __init__(self, path = '', sound = None):
+ def __init__(self, path='', sound=None):
self.path = path
self.sound = sound
@@ -107,7 +108,7 @@ class SoundInfo:
self.duration = duration
return duration
- def get_sound(self, save = True, **kwargs):
+ def get_sound(self, save=True, **kwargs):
"""
Get or create a sound using self info.
@@ -115,8 +116,8 @@ class SoundInfo:
(if save is True, sync to DB), and check for a playlist file.
"""
sound, created = Sound.objects.get_or_create(
- path = self.path,
- defaults = kwargs
+ path=self.path,
+ defaults=kwargs
)
if created or sound.check_on_file():
logger.info('sound is new or have been modified -> %s', self.path)
@@ -127,7 +128,7 @@ class SoundInfo:
self.sound = sound
return sound
- def find_playlist(self, sound, use_default = True):
+ def find_playlist(self, sound, use_default=True):
"""
Find a playlist file corresponding to the sound path, such as:
my_sound.ogg => my_sound.csv
@@ -135,11 +136,11 @@ class SoundInfo:
If use_default is True and there is no playlist find found,
use sound file's metadata.
"""
- if sound.tracks.count():
+ if sound.track_set.count():
return
import aircox.management.commands.import_playlist \
- as import_playlist
+ as import_playlist
# no playlist, try to retrieve metadata
path = os.path.splitext(self.sound.path)[0] + '.csv'
@@ -151,9 +152,9 @@ class SoundInfo:
return
# else, import
- import_playlist.Importer(sound, path, save=True)
+ import_playlist.Importer(path, sound=sound).run()
- def find_diffusion(self, program, save = True):
+ def find_diffusion(self, program, save=True):
"""
For a given program, check if there is an initial diffusion
to associate to, using the date info we have. Update self.sound
@@ -163,7 +164,7 @@ class SoundInfo:
rerun.
"""
if self.year == None or not self.sound or self.sound.diffusion:
- return;
+ return
if self.hour is None:
date = datetime.date(self.year, self.month, self.day)
@@ -173,7 +174,7 @@ class SoundInfo:
date = tz.get_current_timezone().localize(date)
qs = Diffusion.objects.station(program.station).after(date) \
- .filter(program = program, initial__isnull = True)
+ .filter(program=program, initial__isnull=True)
diffusion = qs.first()
if not diffusion:
return
@@ -190,18 +191,19 @@ class MonitorHandler(PatternMatchingEventHandler):
"""
Event handler for watchdog, in order to be used in monitoring.
"""
+
def __init__(self, subdir):
"""
subdir: AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR
"""
self.subdir = subdir
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
- self.sound_kwargs = { 'type': Sound.Type.archive }
+ self.sound_kwargs = {'type': Sound.Type.archive}
else:
- self.sound_kwargs = { 'type': Sound.Type.excerpt }
+ self.sound_kwargs = {'type': Sound.Type.excerpt}
patterns = ['*/{}/*{}'.format(self.subdir, ext)
- for ext in settings.AIRCOX_SOUND_FILE_EXT ]
+ for ext in settings.AIRCOX_SOUND_FILE_EXT]
super().__init__(patterns=patterns, ignore_directories=True)
def on_created(self, event):
@@ -215,14 +217,14 @@ class MonitorHandler(PatternMatchingEventHandler):
si = SoundInfo(event.src_path)
self.sound_kwargs['program'] = program
- si.get_sound(save = True, **self.sound_kwargs)
+ si.get_sound(save=True, **self.sound_kwargs)
if si.year is not None:
si.find_diffusion(program)
si.sound.save(True)
def on_deleted(self, event):
logger.info('sound deleted: %s', event.src_path)
- sound = Sound.objects.filter(path = event.src_path)
+ sound = Sound.objects.filter(path=event.src_path)
if sound:
sound = sound[0]
sound.type = sound.Type.removed
@@ -230,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def on_moved(self, event):
logger.info('sound moved: %s -> %s', event.src_path, event.dest_path)
- sound = Sound.objects.filter(path = event.src_path)
+ sound = Sound.objects.filter(path=event.src_path)
if not sound:
self.on_modified(
FileModifiedEvent(event.dest_path)
@@ -242,18 +244,19 @@ class MonitorHandler(PatternMatchingEventHandler):
if not sound.diffusion:
program = Program.get_from_path(event.src_path)
if program:
- si = SoundInfo(sound.path, sound = sound)
+ si = SoundInfo(sound.path, sound=sound)
if si.year is not None:
si.find_diffusion(program)
sound.save()
class Command(BaseCommand):
- help= __doc__
+ help = __doc__
- def report(self, program = None, component = None, *content):
+ def report(self, program=None, component=None, *content):
if not component:
- logger.info('%s: %s', str(program), ' '.join([str(c) for c in content]))
+ logger.info('%s: %s', str(program),
+ ' '.join([str(c) for c in content]))
else:
logger.info('%s, %s: %s', str(program), str(component),
' '.join([str(c) for c in content]))
@@ -270,11 +273,11 @@ class Command(BaseCommand):
logger.info('#%d %s', program.id, program.name)
self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
- type = Sound.Type.archive,
+ type=Sound.Type.archive,
)
self.scan_for_program(
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
- type = Sound.Type.excerpt,
+ type=Sound.Type.excerpt,
)
dirs.append(os.path.join(program.path))
@@ -300,14 +303,14 @@ class Command(BaseCommand):
si = SoundInfo(path)
sound_kwargs['program'] = program
- si.get_sound(save = True, **sound_kwargs)
- si.find_diffusion(program, save = True)
+ si.get_sound(save=True, **sound_kwargs)
+ si.find_diffusion(program, save=True)
si.find_playlist(si.sound)
sounds.append(si.sound.pk)
# sounds in db & unchecked
- sounds = Sound.objects.filter(path__startswith = subdir). \
- exclude(pk__in = sounds)
+ sounds = Sound.objects.filter(path__startswith=subdir). \
+ exclude(pk__in=sounds)
self.check_sounds(sounds)
@staticmethod
@@ -318,18 +321,18 @@ class Command(BaseCommand):
# check files
for sound in qs:
if sound.check_on_file():
- sound.save(check = False)
+ sound.save(check=False)
- def check_quality(self, check = False):
+ def check_quality(self, check=False):
"""
Check all files where quality has been set to bad
"""
import aircox.management.commands.sounds_quality_check \
- as quality_check
+ as quality_check
# get available sound files
- sounds = Sound.objects.filter(good_quality = False) \
- .exclude(type = Sound.Type.removed)
+ sounds = Sound.objects.filter(good_quality=False) \
+ .exclude(type=Sound.Type.removed)
if check:
self.check_sounds(sounds)
@@ -341,11 +344,12 @@ class Command(BaseCommand):
# check quality
logger.info('quality check...',)
cmd = quality_check.Command()
- cmd.handle( files = files,
- **settings.AIRCOX_SOUND_QUALITY )
+ cmd.handle(files=files,
+ **settings.AIRCOX_SOUND_QUALITY)
# update stats
logger.info('update stats in database')
+
def update_stats(sound_info, sound):
stats = sound_info.get_file_stats()
if stats:
@@ -353,25 +357,25 @@ class Command(BaseCommand):
sound.duration = utils.seconds_to_time(duration)
for sound_info in cmd.good:
- sound = Sound.objects.get(path = sound_info.path)
+ sound = Sound.objects.get(path=sound_info.path)
sound.good_quality = True
update_stats(sound_info, sound)
- sound.save(check = False)
+ sound.save(check=False)
for sound_info in cmd.bad:
- sound = Sound.objects.get(path = sound_info.path)
+ sound = Sound.objects.get(path=sound_info.path)
update_stats(sound_info, sound)
- sound.save(check = False)
+ sound.save(check=False)
def monitor(self):
"""
Run in monitor mode
"""
archives_handler = MonitorHandler(
- subdir = settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
+ subdir=settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
)
excerpts_handler = MonitorHandler(
- subdir = settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
+ subdir=settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
)
observer = Observer()
@@ -390,10 +394,10 @@ class Command(BaseCommand):
time.sleep(1)
def add_arguments(self, parser):
- parser.formatter_class=RawTextHelpFormatter
+ parser.formatter_class = RawTextHelpFormatter
parser.add_argument(
'-q', '--quality_check', action='store_true',
- help='Enable quality check using sound_quality_check on all ' \
+ help='Enable quality check using sound_quality_check on all '
'sounds marqued as not good'
)
parser.add_argument(
@@ -411,7 +415,6 @@ class Command(BaseCommand):
if options.get('scan'):
self.scan()
if options.get('quality_check'):
- self.check_quality(check = (not options.get('scan')) )
+ self.check_quality(check=(not options.get('scan')))
if options.get('monitor'):
self.monitor()
-
diff --git a/aircox/management/commands/streamer.py b/aircox/management/commands/streamer.py
index 9b0d37d..132fff7 100755
--- a/aircox/management/commands/streamer.py
+++ b/aircox/management/commands/streamer.py
@@ -82,17 +82,14 @@ class Monitor:
"""
Last sound log of monitored station that occurred on_air
"""
- return self.get_last_log(type = Log.Type.on_air,
- sound__isnull = False)
+ return self.get_last_log(type=Log.Type.on_air, sound__isnull=False)
@property
def last_diff_start(self):
"""
Log of last triggered item (sound or diffusion)
"""
- return self.get_last_log(type = Log.Type.start,
- diffusion__isnull = False)
-
+ return self.get_last_log(type=Log.Type.start, diffusion__isnull=False)
def __init__(self, station, **kwargs):
self.station = station
@@ -120,12 +117,11 @@ class Monitor:
self.sync_playlists()
self.handle()
- def log(self, date = None, **kwargs):
+ def log(self, date=None, **kwargs):
"""
Create a log using **kwargs, and print info
"""
- log = Log(station = self.station, date = date or tz.now(),
- **kwargs)
+ log = Log(station=self.station, date=date or tz.now(), **kwargs)
log.save()
log.print()
return log
@@ -142,14 +138,14 @@ class Monitor:
air_times = (air_time - delta, air_time + delta)
log = self.log_qs.on_air().filter(
- source = source.id, sound__path = sound_path,
- date__range = air_times,
+ source=source.id, sound__path=sound_path,
+ date__range=air_times,
).last()
if log:
return log
# get sound
- sound = Sound.objects.filter(path = sound_path) \
+ sound = Sound.objects.filter(path=sound_path) \
.select_related('diffusion').first()
diff = None
if sound and sound.diffusion:
@@ -157,20 +153,16 @@ class Monitor:
# check for reruns
if not diff.is_date_in_range(air_time) and not diff.initial:
diff = Diffusion.objects.at(air_time) \
- .filter(initial = diff).first()
+ .filter(initial=diff).first()
# log sound on air
return self.log(
- type = Log.Type.on_air,
- source = source.id,
- date = source.on_air,
- sound = sound,
- diffusion = diff,
+ type=Log.Type.on_air, source=source.id, date=source.on_air,
+ sound=sound, diffusion=diff,
# if sound is removed, we keep sound path info
- comment = sound_path,
+ comment=sound_path,
)
-
def trace_tracks(self, log):
"""
Log tracks for the given sound log (for streamed programs only).
@@ -178,23 +170,21 @@ class Monitor:
if log.diffusion:
return
- tracks = Track.objects.related(object = log.sound) \
- .filter(in_seconds = True)
+ tracks = Track.objects.filter(sound=log.sound, timestamp_isnull=False)
if not tracks.exists():
return
- tracks = tracks.exclude(log__station = self.station,
- log__pk__gt = log.pk)
+ tracks = tracks.exclude(log__station=self.station, log__pk__gt=log.pk)
now = tz.now()
for track in tracks:
- pos = log.date + tz.timedelta(seconds = track.position)
+ pos = log.date + tz.timedelta(seconds=track.position)
if pos > now:
break
# log track on air
self.log(
- type = Log.Type.on_air, source = log.source,
- date = pos, track = track,
- comment = track,
+ type=Log.Type.on_air, source=log.source,
+ date=pos, track=track,
+ comment=track,
)
def sync_playlists(self):
diff --git a/aircox/models.py b/aircox/models.py
index 4fdb4d6..dc62ab8 100755
--- a/aircox/models.py
+++ b/aircox/models.py
@@ -11,6 +11,7 @@ from django.contrib.contenttypes.fields import (GenericForeignKey,
GenericRelation)
from django.contrib.contenttypes.models import ContentType
from django.db import models
+from django.db.transaction import atomic
from django.template.defaultfilters import slugify
from django.utils import timezone as tz
from django.utils.functional import cached_property
@@ -24,70 +25,10 @@ from taggit.managers import TaggableManager
logger = logging.getLogger('aircox.core')
-#
-# Abstracts
-#
-class RelatedQuerySet(models.QuerySet):
- def related(self, object = None, model = None):
- """
- Return a queryset that filter on the given object or model(s)
-
- * object: if given, use its type and pk; match on models only.
- * model: one model or an iterable of models
- """
-
- if not model and object:
- model = type(object)
-
- qs = self
-
- if hasattr(model, '__iter__'):
- model = [ ContentType.objects.get_for_model(m).id
- for m in model ]
- self = self.filter(related_type__pk__in = model)
- else:
- model = ContentType.objects.get_for_model(model)
- self = self.filter(related_type__pk = model.id)
-
- if object:
- self = self.filter(related_id = object.pk)
-
- return self
-
-class Related(models.Model):
- """
- Add a field "related" of type GenericForeignKey, plus utilities.
- """
- related_type = models.ForeignKey(
- ContentType,
- blank = True, null = True,
- on_delete=models.SET_NULL,
- )
- related_id = models.PositiveIntegerField(
- blank = True, null = True,
- )
- related = GenericForeignKey(
- 'related_type', 'related_id',
- )
-
- class Meta:
- abstract = True
-
- objects = RelatedQuerySet.as_manager()
-
- @classmethod
- def ReverseField(cl):
- """
- Return a GenericRelation object that points to this class
- """
-
- return GenericRelation(cl, 'related_id', 'related_type')
-
-
class Nameable(models.Model):
- name = models.CharField (
+ name = models.CharField(
_('name'),
- max_length = 128,
+ max_length=128,
)
class Meta:
@@ -98,81 +39,30 @@ class Nameable(models.Model):
"""
Slug based on the name. We replace '-' by '_'
"""
-
return slugify(self.name).replace('-', '_')
def __str__(self):
- #if self.pk:
+ # if self.pk:
# return '#{} {}'.format(self.pk, self.name)
-
return '{}'.format(self.name)
-#
-# Small common models
-#
-class Track(Related):
- """
- 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.
- """
- # There are no nice solution for M2M relations ship (even without
- # through) in django-admin. So we unfortunately need to make one-
- # to-one relations and add a position argument
- title = models.CharField (
- _('title'),
- max_length = 128,
- )
- artist = models.CharField(
- _('artist'),
- max_length = 128,
- )
- tags = TaggableManager(
- verbose_name=_('tags'),
- blank=True,
- )
- info = models.CharField(
- _('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.'),
- )
- position = models.SmallIntegerField(
- default = 0,
- help_text=_('position in the playlist'),
- )
- in_seconds = models.BooleanField(
- _('in seconds'),
- default = False,
- help_text=_('position in the playlist is expressed in seconds')
- )
-
- def __str__(self):
- return '{self.artist} -- {self.title} -- {self.position}'.format(self=self)
-
- class Meta:
- verbose_name = _('Track')
- verbose_name_plural = _('Tracks')
-
#
# Station related classes
#
class StationQuerySet(models.QuerySet):
- def default(self, station = None):
+ def default(self, station=None):
"""
Return station model instance, using defaults or
given one.
"""
-
if station is None:
return self.order_by('-default', 'pk').first()
+ return self.filter(pk=station).first()
- return self.filter(pk = station).first()
def default_station():
""" Return default station (used by model fields) """
-
return Station.objects.default()
@@ -187,14 +77,14 @@ class Station(Nameable):
"""
path = models.CharField(
_('path'),
- help_text = _('path to the working directory'),
- max_length = 256,
- blank = True,
+ help_text=_('path to the working directory'),
+ max_length=256,
+ blank=True,
)
default = models.BooleanField(
_('default station'),
- default = True,
- help_text = _('if checked, this station is used as the main one')
+ default=True,
+ help_text=_('if checked, this station is used as the main one')
)
objects = StationQuerySet.as_manager()
@@ -208,14 +98,13 @@ class Station(Nameable):
def __prepare_controls(self):
import aircox.controllers as controllers
-
if not self.__streamer:
- self.__streamer = controllers.Streamer(station = self)
- self.__dealer = controllers.Source(station = self)
- self.__sources = [ self.__dealer ] + [
- controllers.Source(station = self, program = program)
+ self.__streamer = controllers.Streamer(station=self)
+ self.__dealer = controllers.Source(station=self)
+ self.__sources = [self.__dealer] + [
+ controllers.Source(station=self, program=program)
- for program in Program.objects.filter(stream__isnull = False)
+ for program in Program.objects.filter(stream__isnull=False)
]
@property
@@ -223,10 +112,9 @@ class Station(Nameable):
"""
Return all active input ports of the station
"""
-
return self.port_set.filter(
- direction = Port.Direction.input,
- active = True
+ direction=Port.Direction.input,
+ active=True
)
@property
@@ -234,10 +122,9 @@ class Station(Nameable):
"""
Return all active output ports of the station
"""
-
return self.port_set.filter(
- direction = Port.Direction.output,
- active = True,
+ direction=Port.Direction.output,
+ active=True,
)
@property
@@ -246,13 +133,11 @@ class Station(Nameable):
Audio sources, dealer included
"""
self.__prepare_controls()
-
return self.__sources
@property
def dealer(self):
self.__prepare_controls()
-
return self.__dealer
@property
@@ -261,10 +146,9 @@ class Station(Nameable):
Audio controller for the station
"""
self.__prepare_controls()
-
return self.__streamer
- def on_air(self, date = None, count = 0, no_cache = False):
+ def on_air(self, date=None, count=0):
"""
Return a queryset of what happened on air, based on logs and
diffusions informations. The queryset is sorted by -date.
@@ -279,55 +163,47 @@ class Station(Nameable):
that has been played when there was a live diffusion.
"""
# TODO argument to get sound instead of tracks
-
if not date and not count:
raise ValueError('at least one argument must be set')
# FIXME can be a potential source of bug
-
if date:
- date = utils.cast_date(date, to_datetime = False)
-
+ date = utils.cast_date(date, to_datetime=False)
if date and date > datetime.date.today():
return []
now = tz.now()
-
if date:
logs = Log.objects.at(date)
diffs = Diffusion.objects.station(self).at(date) \
- .filter(start__lte = now, type = Diffusion.Type.normal) \
- .order_by('-start')
+ .filter(start__lte=now, type=Diffusion.Type.normal) \
+ .order_by('-start')
else:
logs = Log.objects
diffs = Diffusion.objects \
- .filter(type = Diffusion.Type.normal,
- start__lte = now) \
+ .filter(type=Diffusion.Type.normal,
+ start__lte=now) \
.order_by('-start')[:count]
- q = models.Q(diffusion__isnull = False) | \
- models.Q(track__isnull = False)
+ q = models.Q(diffusion__isnull=False) | \
+ models.Q(track__isnull=False)
logs = logs.station(self).on_air().filter(q).order_by('-date')
# filter out tracks played when there was a diffusion
- n = 0
- q = models.Q()
-
+ n, q = 0, models.Q()
for diff in diffs:
if count and n >= count:
break
# FIXME: does not catch tracks started before diff end but
# that continued afterwards
- q = q | models.Q(date__gte = diff.start, date__lte = diff.end)
+ q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
n += 1
- logs = logs.exclude(q, diffusion__isnull = True)
-
+ logs = logs.exclude(q, diffusion__isnull=True)
if count:
logs = logs[:count]
-
return logs
- def save(self, make_sources = True, *args, **kwargs):
+ def save(self, make_sources=True, *args, **kwargs):
if not self.path:
self.path = os.path.join(
settings.AIRCOX_CONTROLLERS_WORKING_DIR,
@@ -335,20 +211,20 @@ class Station(Nameable):
)
if self.default:
- qs = Station.objects.filter(default = True)
+ qs = Station.objects.filter(default=True)
if self.pk:
- qs = qs.exclude(pk = self.pk)
- qs.update(default = False)
+ qs = qs.exclude(pk=self.pk)
+ qs.update(default=False)
super().save(*args, **kwargs)
class ProgramManager(models.Manager):
- def station(self, station, qs = None, **kwargs):
+ def station(self, station, qs=None, **kwargs):
qs = self if qs is None else qs
- return qs.filter(station = station, **kwargs)
+ return qs.filter(station=station, **kwargs)
class Program(Nameable):
@@ -366,39 +242,39 @@ class Program(Nameable):
"""
station = models.ForeignKey(
Station,
- verbose_name = _('station'),
+ verbose_name=_('station'),
on_delete=models.CASCADE,
)
active = models.BooleanField(
_('active'),
- default = True,
- help_text = _('if not checked this program is no longer active')
+ default=True,
+ help_text=_('if not checked this program is no longer active')
)
sync = models.BooleanField(
_('syncronise'),
- default = True,
- help_text = _('update later diffusions according to schedule changes')
+ default=True,
+ help_text=_('update later diffusions according to schedule changes')
)
objects = ProgramManager()
+ # TODO: use unique slug
@property
def path(self):
"""
Return the path to the programs directory
"""
-
return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
- self.slug + '_' + str(self.id) )
+ self.slug + '_' + str(self.id))
- def ensure_dir(self, subdir = None):
+ def ensure_dir(self, subdir=None):
"""
Make sur the program's dir exists (and optionally subdir). Return True
if the dir (or subdir) exists.
"""
path = os.path.join(self.path, subdir) if subdir else \
- self.path
- os.makedirs(path, exist_ok = True)
+ self.path
+ os.makedirs(path, exist_ok=True)
return os.path.exists(path)
@@ -418,10 +294,10 @@ class Program(Nameable):
"""
Return the first schedule that matches a given date.
"""
- schedules = Schedule.objects.filter(program = self)
+ schedules = Schedule.objects.filter(program=self)
for schedule in schedules:
- if schedule.match(date, check_time = False):
+ if schedule.match(date, check_time=False):
return schedule
def __init__(self, *kargs, **kwargs):
@@ -441,7 +317,8 @@ class Program(Nameable):
self.id, self.name)
shutil.move(self.__original_path, self.path)
- sounds = Sounds.objects.filter(path__startswith = self.__original_path)
+ sounds = Sound.objects.filter(
+ path__startswith=self.__original_path)
for sound in sounds:
sound.path.replace(self.__original_path, self.path)
@@ -455,16 +332,18 @@ class Program(Nameable):
"""
path = path.replace(settings.AIRCOX_PROGRAMS_DIR, '')
- while path[0] == '/': path = path[1:]
+ while path[0] == '/':
+ path = path[1:]
- while path[-1] == '/': path = path[:-2]
+ while path[-1] == '/':
+ path = path[:-2]
if '/' in path:
path = path[:path.index('/')]
path = path.split('_')
path = path[-1]
- qs = cl.objects.filter(id = int(path))
+ qs = cl.objects.filter(id=int(path))
return qs[0] if qs else None
@@ -483,25 +362,25 @@ class Stream(models.Model):
"""
program = models.ForeignKey(
Program,
- verbose_name = _('related program'),
+ verbose_name=_('related program'),
on_delete=models.CASCADE,
)
delay = models.TimeField(
_('delay'),
- blank = True, null = True,
- help_text = _('minimal delay between two sound plays')
+ 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')
+ 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')
+ blank=True, null=True,
+ help_text=_('used to define a time range this stream is'
+ 'played')
)
@@ -529,57 +408,51 @@ class Schedule(models.Model):
one_on_two = 0b100000
program = models.ForeignKey(
- Program,
- verbose_name = _('related program'),
- on_delete=models.CASCADE,
+ Program, models.CASCADE,
+ verbose_name=_('related program'),
)
time = models.TimeField(
_('time'),
- blank = True, null = True,
- help_text = _('start time'),
+ blank=True, null=True,
+ help_text=_('start time'),
)
date = models.DateField(
_('date'),
- blank = True, null = True,
- help_text = _('date of the first diffusion'),
+ blank=True, null=True,
+ help_text=_('date of the first diffusion'),
)
timezone = models.CharField(
_('timezone'),
- default = tz.get_current_timezone,
- choices = [(x, x) for x in pytz.all_timezones],
- max_length = 100,
- help_text = _('timezone used for the date')
+ default=tz.get_current_timezone, max_length=100,
+ choices=[(x, x) for x in pytz.all_timezones],
+ help_text=_('timezone used for the date')
)
duration = models.TimeField(
_('duration'),
- help_text = _('regular duration'),
+ help_text=_('regular duration'),
)
frequency = models.SmallIntegerField(
_('frequency'),
- choices = [
- (int(y), {
- 'ponctual': _('ponctual'),
- 'first': _('first week of the month'),
- 'second': _('second week of the month'),
- 'third': _('third week of the month'),
- 'fourth': _('fourth week of the month'),
- 'last': _('last week of the month'),
- 'first_and_third': _('first and third weeks of the month'),
- 'second_and_fourth': _('second and fourth weeks of the month'),
- 'every': _('every week'),
- 'one_on_two': _('one week on two'),
- }[x]) for x,y in Frequency.__members__.items()
- ],
+ choices=[(int(y), {
+ 'ponctual': _('ponctual'),
+ 'first': _('first week of the month'),
+ 'second': _('second week of the month'),
+ 'third': _('third week of the month'),
+ 'fourth': _('fourth week of the month'),
+ 'last': _('last week of the month'),
+ 'first_and_third': _('first and third weeks of the month'),
+ 'second_and_fourth': _('second and fourth weeks of the month'),
+ 'every': _('every week'),
+ 'one_on_two': _('one week on two'),
+ }[x]) for x, y in Frequency.__members__.items()],
)
initial = models.ForeignKey(
- 'self',
- verbose_name = _('initial schedule'),
- blank = True, null = True,
- on_delete=models.SET_NULL,
- help_text = _('this schedule is a rerun of this one'),
+ 'self', models.SET_NULL,
+ verbose_name=_('initial schedule'),
+ blank=True, null=True,
+ help_text=_('this schedule is a rerun of this one'),
)
-
@cached_property
def tz(self):
"""
@@ -592,7 +465,7 @@ class Schedule(models.Model):
# 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:
@@ -606,7 +479,7 @@ class Schedule(models.Model):
return False
- def match(self, date = None, check_time = True):
+ def match(self, date=None, check_time=True):
"""
Return True if the given datetime matches the schedule
"""
@@ -622,11 +495,10 @@ class Schedule(models.Model):
# we check against a normalized version (norm_date will have
# schedule's date.
- norm_date = self.normalize(date)
- return date == norm_date
+ return date == self.normalize(date)
- def match_week(self, date = None):
+ def match_week(self, date=None):
"""
Return True if the given week number matches the schedule, False
otherwise.
@@ -638,17 +510,18 @@ class Schedule(models.Model):
# since we care only about the week, go to the same day of the week
date = utils.date_or_default(date)
- date += tz.timedelta(days = self.date.weekday() - date.weekday() )
+ date += tz.timedelta(days=self.date.weekday() - date.weekday())
# FIXME this case
if self.frequency == Schedule.Frequency.one_on_two:
# cf notes in date_of_month
- diff = utils.cast_date(date, False) - utils.cast_date(self.date, False)
+ diff = utils.cast_date(date, False) - \
+ utils.cast_date(self.date, False)
return not (diff.days % 14)
- first_of_month = date.replace(day = 1)
+ first_of_month = date.replace(day=1)
week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
# weeks of month
@@ -673,7 +546,7 @@ class Schedule(models.Model):
return date
- def dates_of_month(self, date = None):
+ def dates_of_month(self, date=None):
"""
Return a list with all matching dates of date.month (=today)
Ensure timezone awareness.
@@ -683,22 +556,23 @@ class Schedule(models.Model):
return []
# first day of month
- date = utils.date_or_default(date, to_datetime = False) \
+ date = utils.date_or_default(date, to_datetime=False) \
.replace(day=1)
freq = self.frequency
# last of the month
if freq == Schedule.Frequency.last:
- date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
+ date = date.replace(
+ day=calendar.monthrange(date.year, date.month)[1])
# end of month before the wanted weekday: move one week back
if date.weekday() < self.date.weekday():
- date -= tz.timedelta(days = 7)
+ date -= tz.timedelta(days=7)
delta = self.date.weekday() - date.weekday()
- date += tz.timedelta(days = delta)
+ date += tz.timedelta(days=delta)
return [self.normalize(date)]
@@ -706,34 +580,35 @@ class Schedule(models.Model):
# check on SO#3284452 for the formula
first_weekday = date.weekday()
sched_weekday = self.date.weekday()
- date += tz.timedelta(days = (7 if first_weekday > sched_weekday else 0) \
- - first_weekday + sched_weekday)
+ date += tz.timedelta(days=(7 if first_weekday > sched_weekday else 0)
+ - first_weekday + sched_weekday)
month = date.month
dates = []
if freq == Schedule.Frequency.one_on_two:
# check date base on a diff of dates base on a 14 days delta
- diff = utils.cast_date(date, False) - utils.cast_date(self.date, False)
+ diff = utils.cast_date(date, False) - \
+ utils.cast_date(self.date, False)
if diff.days % 14:
- date += tz.timedelta(days = 7)
+ date += tz.timedelta(days=7)
while date.month == month:
dates.append(date)
- date += tz.timedelta(days = 14)
+ date += tz.timedelta(days=14)
else:
week = 0
while week < 5 and date.month == month:
if freq & (0b1 << week):
dates.append(date)
- date += tz.timedelta(days = 7)
- week += 1;
+ date += tz.timedelta(days=7)
+ week += 1
return [self.normalize(date) for date in dates]
- def diffusions_of_month(self, date = None, exclude_saved = False):
+ def diffusions_of_month(self, date=None, exclude_saved=False):
"""
Return a list of Diffusion instances, from month of the given date, that
can be not in the database.
@@ -750,7 +625,7 @@ class Schedule(models.Model):
# existing diffusions
for item in Diffusion.objects.filter(
- program = self.program, start__in = dates):
+ program=self.program, start__in=dates):
if item.start in dates:
dates.remove(item.start)
@@ -765,13 +640,12 @@ class Schedule(models.Model):
delta = self.date - self.initial.date
diffusions += [
Diffusion(
- program = self.program,
- type = Diffusion.Type.unconfirmed,
- initial = \
- Diffusion.objects.filter(start = date - delta).first() \
- if self.initial else None,
- start = date,
- end = date + duration,
+ program=self.program,
+ type=Diffusion.Type.unconfirmed,
+ initial=Diffusion.objects.filter(start=date - delta).first()
+ if self.initial else None,
+ start=date,
+ end=date + duration,
) for date in dates
]
@@ -786,9 +660,9 @@ class Schedule(models.Model):
self.__initial = self.__dict__.copy()
def __str__(self):
- return ' | '.join([ '#' + str(self.id), self.program.name,
- self.get_frequency_display(),
- self.time.strftime('%a %H:%M') ])
+ return ' | '.join(['#' + str(self.id), self.program.name,
+ self.get_frequency_display(),
+ self.time.strftime('%a %H:%M')])
def save(self, *args, **kwargs):
if self.initial:
@@ -806,12 +680,12 @@ class Schedule(models.Model):
class DiffusionQuerySet(models.QuerySet):
def station(self, station, **kwargs):
- return self.filter(program__station = station, **kwargs)
+ return self.filter(program__station=station, **kwargs)
def program(self, program):
- return self.filter(program = program)
+ return self.filter(program=program)
- def at(self, date = None, next = False, **kwargs):
+ def at(self, date=None, next=False, **kwargs):
"""
Return diffusions occuring at the given date, ordered by +start
@@ -825,7 +699,7 @@ class DiffusionQuerySet(models.QuerySet):
the given moment.
"""
# note: we work with localtime
- date = utils.date_or_default(date, keep_type = True)
+ date = utils.date_or_default(date, keep_type=True)
qs = self
filters = None
@@ -833,44 +707,44 @@ class DiffusionQuerySet(models.QuerySet):
if isinstance(date, datetime.datetime):
# use datetime: we want diffusion that occurs around this
# range
- filters = { 'start__lte': date, 'end__gte': date }
+ filters = {'start__lte': date, 'end__gte': date}
if next:
qs = qs.filter(
- models.Q(start__gte = date) | models.Q(**filters)
+ models.Q(start__gte=date) | models.Q(**filters)
)
else:
qs = qs.filter(**filters)
else:
# use date: we want diffusions that occurs this day
start, end = utils.date_range(date)
- filters = models.Q(start__gte = start, start__lte = end) | \
- models.Q(end__gt = start, end__lt = end)
+ filters = models.Q(start__gte=start, start__lte=end) | \
+ models.Q(end__gt=start, end__lt=end)
if next:
# include also diffusions of the next day
- filters |= models.Q(start__gte = start)
+ filters |= models.Q(start__gte=start)
qs = qs.filter(filters, **kwargs)
return qs.order_by('start').distinct()
- def after(self, date = None, **kwargs):
+ def after(self, date=None, **kwargs):
"""
Return a queryset of diffusions that happen after the given
date.
"""
- date = utils.date_or_default(date, keep_type = True)
+ date = utils.date_or_default(date, keep_type=True)
- return self.filter(start__gte = date, **kwargs).order_by('start')
+ return self.filter(start__gte=date, **kwargs).order_by('start')
- def before(self, date = None, **kwargs):
+ def before(self, date=None, **kwargs):
"""
Return a queryset of diffusions that finish before the given
date.
"""
date = utils.date_or_default(date)
- return self.filter(end__lte = date, **kwargs).order_by('start')
+ return self.filter(end__lte=date, **kwargs).order_by('start')
class Diffusion(models.Model):
@@ -899,22 +773,22 @@ class Diffusion(models.Model):
canceled = 0x02
# common
- program = models.ForeignKey (
+ program = models.ForeignKey(
Program,
- verbose_name = _('program'),
+ verbose_name=_('program'),
on_delete=models.CASCADE,
)
# specific
type = models.SmallIntegerField(
- verbose_name = _('type'),
- choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
+ verbose_name=_('type'),
+ choices=[(int(y), _(x)) for x, y in Type.__members__.items()],
)
- initial = models.ForeignKey (
+ initial = models.ForeignKey(
'self', on_delete=models.SET_NULL,
- blank = True, null = True,
- related_name = 'reruns',
- verbose_name = _('initial diffusion'),
- help_text = _('the diffusion is a rerun of this one')
+ blank=True, null=True,
+ related_name='reruns',
+ verbose_name=_('initial diffusion'),
+ help_text=_('the diffusion is a rerun of this one')
)
# port = models.ForeignKey(
# 'self',
@@ -925,15 +799,13 @@ class Diffusion(models.Model):
# )
conflicts = models.ManyToManyField(
'self',
- verbose_name = _('conflicts'),
- blank = True,
- help_text = _('conflicts'),
+ verbose_name=_('conflicts'),
+ blank=True,
+ help_text=_('conflicts'),
)
- start = models.DateTimeField( _('start of the diffusion') )
- end = models.DateTimeField( _('end of the diffusion') )
-
- tracks = Track.ReverseField()
+ start = models.DateTimeField(_('start of the diffusion'))
+ end = models.DateTimeField(_('end of the diffusion'))
@property
def duration(self):
@@ -979,8 +851,7 @@ class Diffusion(models.Model):
"""
return self.type == self.Type.normal and \
- not self.get_sounds(archive = True).count()
-
+ not self.get_sounds(archive=True).count()
def get_playlist(self, **types):
"""
@@ -988,10 +859,10 @@ class Diffusion(models.Model):
The given arguments are passed to ``get_sounds``.
"""
- 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):
"""
@@ -1001,12 +872,12 @@ class Diffusion(models.Model):
**types: filter on the given sound types name, as `archive=True`
"""
sounds = (self.initial or self).sound_set.order_by('type', 'path')
- _in = [ getattr(Sound.Type, name)
- for name, value in types.items() if value ]
+ _in = [getattr(Sound.Type, name)
+ for name, value in types.items() if value]
- return sounds.filter(type__in = _in)
+ return sounds.filter(type__in=_in)
- def is_date_in_range(self, date = None):
+ def is_date_in_range(self, date=None):
"""
Return true if the given date is in the diffusion's start-end
range.
@@ -1021,11 +892,11 @@ class Diffusion(models.Model):
"""
return Diffusion.objects.filter(
- models.Q(start__lt = self.start,
- end__gt = self.start) |
- models.Q(start__gt = self.start,
- start__lt = self.end)
- )
+ models.Q(start__lt=self.start,
+ end__gt=self.start) |
+ models.Q(start__gt=self.start,
+ start__lt=self.end)
+ ).exclude(pk=self.pk).distinct()
def check_conflicts(self):
conflicts = self.get_conflicts()
@@ -1040,7 +911,7 @@ class Diffusion(models.Model):
'end': self.end,
}
- def save(self, no_check = False, *args, **kwargs):
+ def save(self, no_check=False, *args, **kwargs):
if no_check:
return super().save(*args, **kwargs)
@@ -1056,7 +927,6 @@ class Diffusion(models.Model):
self.end != self.__initial['end']:
self.check_conflicts()
-
def __str__(self):
return '{self.program.name} {date} #{self.pk}'.format(
self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
@@ -1065,7 +935,6 @@ class Diffusion(models.Model):
class Meta:
verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions')
-
permissions = (
('programming', _('edit the diffusion\'s planification')),
)
@@ -1084,63 +953,61 @@ class Sound(Nameable):
program = models.ForeignKey(
Program,
- verbose_name = _('program'),
- blank = True, null = True,
+ verbose_name=_('program'),
+ blank=True, null=True,
on_delete=models.SET_NULL,
- help_text = _('program related to it'),
+ help_text=_('program related to it'),
)
diffusion = models.ForeignKey(
'Diffusion',
- verbose_name = _('diffusion'),
- blank = True, null = True,
+ verbose_name=_('diffusion'),
+ blank=True, null=True,
on_delete=models.SET_NULL,
- help_text = _('initial diffusion related it')
+ help_text=_('initial diffusion related it')
)
type = models.SmallIntegerField(
- verbose_name = _('type'),
- choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
- blank = True, null = True
+ verbose_name=_('type'),
+ choices=[(int(y), _(x)) for x, y in Type.__members__.items()],
+ blank=True, null=True
)
# FIXME: url() does not use the same directory than here
# should we use FileField for more reliability?
path = models.FilePathField(
_('file'),
- path = settings.AIRCOX_PROGRAMS_DIR,
- match = r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT)
- .replace('.', r'\.') + ')$',
- recursive = True,
- blank = True, null = True,
- unique = True,
- max_length = 255
+ path=settings.AIRCOX_PROGRAMS_DIR,
+ match=r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT)
+ .replace('.', r'\.') + ')$',
+ recursive=True,
+ blank=True, null=True,
+ unique=True,
+ max_length=255
)
embed = models.TextField(
_('embed HTML code'),
- blank = True, null = True,
- help_text = _('HTML code used to embed a sound from external plateform'),
+ blank=True, null=True,
+ help_text=_('HTML code used to embed a sound from external plateform'),
)
duration = models.TimeField(
_('duration'),
- blank = True, null = True,
- help_text = _('duration of the sound'),
+ 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'),
+ blank=True, null=True,
+ help_text=_('last modification date and time'),
)
good_quality = models.NullBooleanField(
_('good quality'),
- help_text = _('sound\'s quality is okay'),
- blank = True, null = True
+ help_text=_('sound\'s quality is okay'),
+ blank=True, null=True
)
public = models.BooleanField(
_('public'),
- default = False,
- help_text = _('the sound is accessible to the public')
+ default=False,
+ help_text=_('the sound is accessible to the public')
)
- tracks = Track.ReverseField()
-
def get_mtime(self):
"""
Get the last modification date from file
@@ -1148,7 +1015,7 @@ class Sound(Nameable):
mtime = os.stat(self.path).st_mtime
mtime = tz.datetime.fromtimestamp(mtime)
# db does not store microseconds
- mtime = mtime.replace(microsecond = 0)
+ mtime = mtime.replace(microsecond=0)
return tz.make_aware(mtime, tz.get_current_timezone())
@@ -1174,7 +1041,6 @@ class Sound(Nameable):
Get metadata from sound file and return a Track object if succeed,
else None.
"""
-
if not self.file_exists():
return None
@@ -1189,24 +1055,19 @@ class Sound(Nameable):
def get_meta(key, cast=str):
value = meta.get(key)
-
return cast(value[0]) if value else None
info = '{} ({})'.format(get_meta('album'), get_meta('year')) \
- if meta and ('album' and 'year' in meta) else \
+ if meta and ('album' and 'year' in meta) else \
get_meta('album') \
- if 'album' else \
+ if 'album' else \
('year' in meta) and get_meta('year') or ''
- track = Track(
- related = self,
- title = get_meta('title') or self.name,
- artist = get_meta('artist') or _('unknown'),
- info = info,
- position = get_meta('tracknumber', int) or 0,
- )
-
- return track
+ return Track(sound=self,
+ position=get_meta('tracknumber', int) or 0,
+ title=get_meta('title') or self.name,
+ artist=get_meta('artist') or _('unknown'),
+ info=info)
def check_on_file(self):
"""
@@ -1229,7 +1090,7 @@ class Sound(Nameable):
changed = True
self.type = self.Type.archive \
if self.path.startswith(self.program.archives_path) else \
- self.Type.excerpt
+ self.Type.excerpt
# check mtime -> reset quality if changed (assume file changed)
mtime = self.get_mtime()
@@ -1276,7 +1137,7 @@ class Sound(Nameable):
super().__init__(*args, **kwargs)
self.__check_name()
- def save(self, check = True, *args, **kwargs):
+ def save(self, check=True, *args, **kwargs):
if check:
self.check_on_file()
self.__check_name()
@@ -1290,6 +1151,64 @@ class Sound(Nameable):
verbose_name_plural = _('Sounds')
+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.
+ """
+ diffusion = models.ForeignKey(
+ Diffusion, models.CASCADE, blank=True, null=True,
+ verbose_name=_('diffusion'),
+ )
+ sound = models.ForeignKey(
+ Sound, models.CASCADE, blank=True, null=True,
+ verbose_name=_('sound'),
+ )
+ position = models.PositiveSmallIntegerField(
+ _('order'),
+ default=0,
+ help_text=_('position in the playlist'),
+ )
+ timestamp = models.PositiveSmallIntegerField(
+ _('timestamp'),
+ blank=True, null=True,
+ help_text=_('position in seconds')
+ )
+ title = models.CharField(
+ _('title'),
+ max_length=128,
+ )
+ artist = models.CharField(
+ _('artist'),
+ max_length=128,
+ )
+ tags = TaggableManager(
+ verbose_name=_('tags'),
+ blank=True,
+ )
+ info = models.CharField(
+ _('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.'),
+ )
+
+ class Meta:
+ verbose_name = _('Track')
+ verbose_name_plural = _('Tracks')
+ ordering = ('position',)
+
+ def __str__(self):
+ return '{self.artist} -- {self.title} -- {self.position}'.format(
+ self=self)
+
+ def save(self, *args, **kwargs):
+ if (self.sound is None and self.diffusion is None) or \
+ (self.sound is not None and self.diffusion is not None):
+ raise ValueError('sound XOR diffusion is required')
+ super().save(*args, **kwargs)
+
#
# Controls and audio input/output
#
@@ -1319,29 +1238,29 @@ class Port (models.Model):
station = models.ForeignKey(
Station,
- verbose_name = _('station'),
+ verbose_name=_('station'),
on_delete=models.CASCADE,
)
direction = models.SmallIntegerField(
_('direction'),
- choices = [ (int(y), _(x)) for x,y in Direction.__members__.items() ],
+ choices=[(int(y), _(x)) for x, y in Direction.__members__.items()],
)
type = models.SmallIntegerField(
_('type'),
# we don't translate the names since it is project names.
- choices = [ (int(y), x) for x,y in Type.__members__.items() ],
+ choices=[(int(y), x) for x, y in Type.__members__.items()],
)
active = models.BooleanField(
_('active'),
- default = True,
- help_text = _('this port is 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
+ 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
)
def is_valid_type(self):
@@ -1368,37 +1287,37 @@ 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 ''
+ direction=self.get_direction_display(),
+ type=self.get_type_display(),
+ id=self.pk or ''
)
class LogQuerySet(models.QuerySet):
def station(self, station):
- return self.filter(station = station)
+ return self.filter(station=station)
- def at(self, date = None):
+ def at(self, date=None):
start, end = utils.date_range(date)
# return qs.filter(models.Q(end__gte = start) |
# models.Q(date__lte = end))
- return self.filter(date__gte = start, date__lte = end)
+ return self.filter(date__gte=start, date__lte=end)
def on_air(self):
- return self.filter(type = Log.Type.on_air)
+ return self.filter(type=Log.Type.on_air)
def start(self):
- return self.filter(type = Log.Type.start)
+ return self.filter(type=Log.Type.start)
- def with_diff(self, with_it = True):
- return self.filter(diffusion__isnull = not with_it)
+ def with_diff(self, with_it=True):
+ return self.filter(diffusion__isnull=not with_it)
- def with_sound(self, with_it = True):
- return self.filter(sound__isnull = not with_it)
+ def with_sound(self, with_it=True):
+ return self.filter(sound__isnull=not with_it)
- def with_track(self, with_it = True):
- return self.filter(track__isnull = not with_it)
+ def with_track(self, with_it=True):
+ return self.filter(track__isnull=not with_it)
@staticmethod
def _get_archive_path(station, date):
@@ -1424,7 +1343,7 @@ class LogQuerySet(models.QuerySet):
rel.pk: rel
for rel in type.objects.filter(
- pk__in = (
+ pk__in=(
log[attr_id]
for log in logs if attr_id in log
@@ -1464,15 +1383,15 @@ class LogQuerySet(models.QuerySet):
# make logs
return [
- Log(diffusion = rel_obj(log, 'diffusion'),
- sound = rel_obj(log, 'sound'),
- track = rel_obj(log, 'track'),
+ Log(diffusion=rel_obj(log, 'diffusion'),
+ sound=rel_obj(log, 'sound'),
+ track=rel_obj(log, 'track'),
**log)
for log in logs
]
- def make_archive(self, station, date, force = False, keep = False):
+ def make_archive(self, station, date, force=False, keep=False):
"""
Archive logs of the given date. If the archive exists, it does
not overwrite it except if "force" is given. In this case, the
@@ -1484,8 +1403,8 @@ class LogQuerySet(models.QuerySet):
import yaml
import gzip
- os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok = True)
- path = self._get_archive_path(station, date);
+ os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True)
+ path = self._get_archive_path(station, date)
if os.path.exists(path) and not force:
return -1
@@ -1496,15 +1415,8 @@ class LogQuerySet(models.QuerySet):
return 0
fields = Log._meta.get_fields()
- logs = [
- {
- i.attname: getattr(log, i.attname)
-
- for i in fields
- }
-
- for log in qs
- ]
+ logs = [{i.attname: getattr(log, i.attname)
+ for i in fields} for log in qs]
# Note: since we use Yaml, we can just append new logs when file
# exists yet <3
@@ -1554,55 +1466,46 @@ class Log(models.Model):
"""
type = models.SmallIntegerField(
- verbose_name = _('type'),
- choices = [ (int(y), _(x.replace('_',' '))) for x,y in Type.__members__.items() ],
- blank = True, null = True,
+ choices=[(int(y), _(x.replace('_', ' ')))
+ for x, y in Type.__members__.items()],
+ blank=True, null=True,
+ verbose_name=_('type'),
)
station = models.ForeignKey(
- Station,
- verbose_name = _('station'),
- on_delete=models.CASCADE,
- help_text = _('related station'),
+ Station, on_delete=models.CASCADE,
+ verbose_name=_('station'),
+ help_text=_('related station'),
)
source = models.CharField(
# we use a CharField to avoid loosing logs information if the
# source is removed
- _('source'),
- max_length=64,
- help_text = _('identifier of the source related to this log'),
- blank = True, null = True,
+ max_length=64, blank=True, null=True,
+ verbose_name=_('source'),
+ help_text=_('identifier of the source related to this log'),
)
date = models.DateTimeField(
- _('date'),
- default=tz.now,
- db_index = True,
+ default=tz.now, db_index=True,
+ verbose_name=_('date'),
)
comment = models.CharField(
- _('comment'),
- max_length = 512,
- blank = True, null = True,
+ max_length=512, blank=True, null=True,
+ verbose_name=_('comment'),
)
diffusion = models.ForeignKey(
- Diffusion,
- verbose_name = _('Diffusion'),
- blank = True, null = True,
- db_index = True,
- on_delete=models.SET_NULL,
+ Diffusion, on_delete=models.SET_NULL,
+ blank=True, null=True, db_index=True,
+ verbose_name=_('Diffusion'),
)
sound = models.ForeignKey(
- Sound,
- verbose_name = _('Sound'),
- blank = True, null = True,
- db_index = True,
- on_delete=models.SET_NULL,
+ Sound, on_delete=models.SET_NULL,
+ blank=True, null=True, db_index=True,
+ verbose_name=_('Sound'),
)
track = models.ForeignKey(
- Track,
- verbose_name = _('Track'),
- blank = True, null = True,
- db_index = True,
- on_delete=models.SET_NULL,
+ Track, on_delete=models.SET_NULL,
+ blank=True, null=True, db_index=True,
+ verbose_name=_('Track'),
)
objects = LogQuerySet.as_manager()
@@ -1618,31 +1521,22 @@ class Log(models.Model):
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())
def print(self):
r = []
-
if self.diffusion:
r.append('diff: ' + str(self.diffusion_id))
-
if self.sound:
r.append('sound: ' + str(self.sound_id))
-
if self.track:
r.append('track: ' + str(self.track_id))
-
- logger.info('log %s: %s%s',
- str(self),
- self.comment or '',
- ' (' + ', '.join(r) + ')' if r else ''
- )
+ logger.info('log %s: %s%s', str(self), self.comment or '',
+ ' (' + ', '.join(r) + ')' if r else '')
def __str__(self):
return '#{} ({}, {}, {})'.format(
- self.pk,
- self.get_type_display(),
- self.source,
- self.local_date.strftime('%Y/%m/%d %H:%M%z'),
+ self.pk, self.get_type_display(),
+ self.source,
+ self.local_date.strftime('%Y/%m/%d %H:%M%z'),
)
diff --git a/aircox/templates/admin/aircox/playlist_inline.html b/aircox/templates/admin/aircox/playlist_inline.html
new file mode 100644
index 0000000..7e16f63
--- /dev/null
+++ b/aircox/templates/admin/aircox/playlist_inline.html
@@ -0,0 +1,6 @@
+{% load static i18n %}
+
+{% with inline_admin_formset.formset.instance as playlist %}
+{% include "adminsortable2/tabular.html" %}
+{% endwith %}
+
diff --git a/aircox/templates/aircox/config/liquidsoap.liq b/aircox/templates/aircox/config/liquidsoap.liq
index 9fc5d78..d2765db 100755
--- a/aircox/templates/aircox/config/liquidsoap.liq
+++ b/aircox/templates/aircox/config/liquidsoap.liq
@@ -78,8 +78,7 @@ A stream is a source that:
- is interactive
{% endcomment %}
def stream (id, file) =
- s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch',
- file)
+ s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch', file)
interactive_source(id, s)
end
{% endblock %}
diff --git a/aircox/views.py b/aircox/views.py
index af2781a..6efa6da 100755
--- a/aircox/views.py
+++ b/aircox/views.py
@@ -25,7 +25,7 @@ class Stations:
if self.fetch_timeout and self.fetch_timeout > tz.now():
return
- self.fetch_timeout = tz.now() + tz.timedelta(seconds = 5)
+ self.fetch_timeout = tz.now() + tz.timedelta(seconds=5)
for station in self.stations:
station.streamer.fetch()
@@ -39,62 +39,54 @@ def on_air(request):
except:
cms = None
- station = request.GET.get('station');
+ station = request.GET.get('station')
if station:
# FIXME: by name???
- station = stations.stations.filter(name = station)
+ station = stations.stations.filter(name=station)
if not station.count():
return HttpResponse('{}')
else:
station = stations.stations
station = station.first()
- on_air = station.on_air(count = 10).select_related('track','diffusion')
+ on_air = station.on_air(count=10).select_related('track', 'diffusion')
if not on_air.count():
return HttpResponse('')
last = on_air.first()
if last.track:
- last = {
- 'type': 'track',
- 'artist': last.related.artist,
- 'title': last.related.title,
- 'date': last.date,
- }
+ last = {'date': last.date, 'type': 'track',
+ 'artist': last.track.artist, 'title': last.track.title}
else:
try:
diff = last.diffusion
publication = None
+ # FIXME CMS
if cms:
publication = \
cms.DiffusionPage.objects.filter(
- diffusion = diff.initial or diff).first() or \
+ diffusion=diff.initial or diff).first() or \
cms.ProgramPage.objects.filter(
- program = last.program).first()
+ program=last.program).first()
except:
pass
-
- last = {
- 'type': 'diffusion',
- 'title': diff.program.name,
- 'date': diff.start,
- 'url': publication.specific.url if publication else None,
- }
-
+ last = {'date': diff.start, 'type': 'diffusion',
+ 'title': diff.program.name,
+ 'url': publication.specific.url if publication else None}
last['date'] = str(last['date'])
return HttpResponse(json.dumps(last))
# TODO:
# - login url
-class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
+class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
template_name = 'aircox/controllers/monitor.html'
def get_context_data(self, **kwargs):
stations.fetch()
- return { 'stations': stations.stations }
+ return {'stations': stations.stations}
- def get(self, request = None, **kwargs):
+ def get(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
@@ -102,7 +94,7 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
context = self.get_context_data(**kwargs)
return render(request, self.template_name, context)
- def post(self, request = None, **kwargs):
+ def post(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
@@ -113,15 +105,15 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
controller = POST.get('controller')
action = POST.get('action')
- station = stations.stations.filter(name = POST.get('station')) \
+ station = stations.stations.filter(name=POST.get('station')) \
.first()
if not station:
return Http404()
source = None
if 'source' in POST:
- source = [ s for s in station.sources
- if s.name == POST['source'] ]
+ source = [s for s in station.sources
+ if s.name == POST['source']]
source = source[0]
if not source:
return Http404
@@ -141,11 +133,11 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
source.restart()
-class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
+class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
"""
View for statistics.
"""
- # we cannot manipulate queryset, since we have to be able to read from archives
+ # we cannot manipulate queryset: we have to be able to read from archives
template_name = 'aircox/controllers/stats.html'
class Item:
@@ -179,7 +171,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
self.__dict__.update(kwargs)
# Note: one row contains a column for diffusions and one for streams
- #def append(self, log):
+ # def append(self, log):
# if log.col == 0:
# self.rows.append((log, []))
# return
@@ -194,13 +186,12 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
# # all other cases: new row
# self.rows.append((None, [log]))
-
def get_stats(self, station, date):
"""
Return statistics for the given station and date.
"""
- stats = self.Stats(station = station, date = date,
- items = [], tags = {})
+ stats = self.Stats(station=station, date=date,
+ items=[], tags={})
qs = Log.objects.station(station).on_air() \
.prefetch_related('diffusion', 'sound', 'track', 'track__tags')
@@ -209,37 +200,26 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
sound_log = None
for log in qs:
- rel = None
- item = None
-
+ rel, item = None, None
if log.diffusion:
- rel = log.diffusion
- item = self.Item(
- name = rel.program.name,
- type = _('Diffusion'),
- col = 0,
- tracks = models.Track.objects.related(object = rel)
- .prefetch_related('tags'),
+ rel, item = log.diffusion, self.Item(
+ name=rel.program.name, type=_('Diffusion'), col=0,
+ tracks=models.Track.objects.filter(diffusion=log.diffusion)
+ .prefetch_related('tags'),
)
sound_log = None
elif log.sound:
- rel = log.sound
- item = self.Item(
- name = rel.program.name + ': ' + os.path.basename(rel.path),
- type = _('Stream'),
- col = 1,
- tracks = [],
+ rel, item = log.sound, self.Item(
+ name=rel.program.name + ': ' + os.path.basename(rel.path),
+ type=_('Stream'), col=1, tracks=[],
)
sound_log = item
elif log.track:
# append to last sound log
if not sound_log:
- # TODO: create item ? should never happen
continue
-
sound_log.tracks.append(log.track)
sound_log.end = log.end
- sound_log
continue
item.date = log.date
@@ -247,7 +227,6 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
item.related = rel
# stats.append(item)
stats.items.append(item)
-
return stats
def get_context_data(self, **kwargs):
@@ -270,7 +249,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
return context
- def get(self, request = None, **kwargs):
+ def get(self, request=None, **kwargs):
if not request.user.is_active:
return Http404()
@@ -279,4 +258,3 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
return render(request, self.template_name, context)
-
diff --git a/aircox_web/__init__.py b/aircox_web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/aircox_web/admin.py b/aircox_web/admin.py
new file mode 100644
index 0000000..2d1c68b
--- /dev/null
+++ b/aircox_web/admin.py
@@ -0,0 +1,68 @@
+import copy
+
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from content_editor.admin import ContentEditor
+from feincms3 import plugins
+from feincms3.admin import TreeAdmin
+
+from aircox import models as aircox
+from . import models
+from aircox.admin.playlist import TracksInline
+from aircox.admin.mixins import UnrelatedInlineMixin
+
+
+@admin.register(models.SiteSettings)
+class SettingsAdmin(admin.ModelAdmin):
+ pass
+
+
+class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
+ parent_model = aircox.Diffusion
+ fields = list(TracksInline.fields)
+ fields.remove('timestamp')
+
+ def get_parent(self, view_obj):
+ return view_obj and view_obj.diffusion
+
+ def save_parent(self, parent, view_obj):
+ parent.save()
+ view_obj.diffusion = parent
+ view_obj.save()
+
+
+@admin.register(models.Page)
+class PageAdmin(ContentEditor, TreeAdmin):
+ list_display = ["indented_title", "move_column", "is_active"]
+ prepopulated_fields = {"slug": ("title",)}
+ # readonly_fields = ('diffusion',)
+
+ fieldsets = (
+ (_('Main'), {
+ 'fields': ['title', 'slug', 'by_program', 'summary'],
+ 'classes': ('tabbed', 'uncollapse')
+ }),
+ (_('Settings'), {
+ 'fields': ['show_author', 'featured', 'allow_comments',
+ 'status', 'static_path', 'path'],
+ 'classes': ('tabbed',)
+ }),
+ (_('Infos'), {
+ 'fields': ['diffusion'],
+ 'classes': ('tabbed',)
+ }),
+ )
+
+ inlines = [
+ plugins.richtext.RichTextInline.create(models.RichText),
+ plugins.image.ImageInline.create(models.Image),
+ ]
+
+ def get_inline_instances(self, request, obj=None):
+ inlines = super().get_inline_instances(request, obj)
+ if obj and obj.diffusion:
+ inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
+ return inlines
+
+
diff --git a/aircox_web/apps.py b/aircox_web/apps.py
new file mode 100644
index 0000000..87affde
--- /dev/null
+++ b/aircox_web/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AircoxWebConfig(AppConfig):
+ name = 'aircox_web'
diff --git a/aircox_web/assets/index.js b/aircox_web/assets/index.js
new file mode 100644
index 0000000..54a2de9
--- /dev/null
+++ b/aircox_web/assets/index.js
@@ -0,0 +1 @@
+import './js';
diff --git a/aircox_web/assets/js/index.js b/aircox_web/assets/js/index.js
new file mode 100644
index 0000000..a4233d5
--- /dev/null
+++ b/aircox_web/assets/js/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Buefy from 'buefy';
+import 'buefy/dist/buefy.css';
+
+Vue.use(Buefy);
+
+var app = new Vue({
+ el: '#app',
+})
+
+
+
diff --git a/aircox_web/models.py b/aircox_web/models.py
new file mode 100644
index 0000000..9f5f947
--- /dev/null
+++ b/aircox_web/models.py
@@ -0,0 +1,117 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth import models as auth
+
+from content_editor.models import Region, create_plugin_base
+from feincms3 import plugins
+from feincms3.pages import AbstractPage
+
+from model_utils.models import TimeStampedModel, StatusModel
+from model_utils import Choices
+from filer.fields.image import FilerImageField
+
+
+from aircox import models as aircox
+
+
+class SiteSettings(models.Model):
+ station = models.ForeignKey(
+ aircox.Station, on_delete=models.SET_NULL, null=True,
+ )
+
+ # main settings
+ title = models.CharField(
+ _('Title'), max_length=32,
+ help_text=_('Website title used at various places'),
+ )
+ logo = FilerImageField(
+ on_delete=models.SET_NULL, null=True, blank=True,
+ verbose_name=_('Logo'),
+ related_name='+',
+ )
+ favicon = FilerImageField(
+ on_delete=models.SET_NULL, null=True, blank=True,
+ verbose_name=_('Favicon'),
+ related_name='+',
+ )
+
+ # meta descriptors
+ description = models.CharField(
+ _('Description'), max_length=128,
+ blank=True, null=True,
+ )
+ tags = models.CharField(
+ _('Tags'), max_length=128,
+ blank=True, null=True,
+ )
+
+
+
+class Page(AbstractPage, TimeStampedModel, StatusModel):
+ STATUS = Choices('draft', 'published')
+ regions = [
+ Region(key="main", title=_("Content")),
+ Region(key="sidebar", title=_("Sidebar")),
+ ]
+
+
+ # metadata
+ by = models.ForeignKey(
+ auth.User, models.SET_NULL, blank=True, null=True,
+ verbose_name=_('Author'),
+ )
+ by_program = models.ForeignKey(
+ aircox.Program, models.SET_NULL, blank=True, null=True,
+ related_name='authored_pages',
+ limit_choices_to={'schedule__isnull': False},
+ verbose_name=_('Show program as author'),
+ help_text=_("If nothing is selected, display user's name"),
+ )
+
+ # options
+ show_author = models.BooleanField(
+ _('Show author'), default=True,
+ )
+ featured = models.BooleanField(
+ _('featured'), default=False,
+ )
+ allow_comments = models.BooleanField(
+ _('allow comments'), default=True,
+ )
+
+ # content
+ title = models.CharField(
+ _('title'), max_length=64,
+ )
+ summary = models.TextField(
+ _('Summary'),
+ max_length=128, blank=True, null=True,
+ )
+ cover = FilerImageField(
+ on_delete=models.SET_NULL, null=True, blank=True,
+ verbose_name=_('Cover'),
+ )
+
+ diffusion = models.OneToOneField(
+ aircox.Diffusion, models.CASCADE,
+ blank=True, null=True,
+ )
+
+PagePlugin = create_plugin_base(Page)
+
+
+class RichText(plugins.richtext.RichText, PagePlugin):
+ pass
+
+
+class Image(plugins.image.Image, PagePlugin):
+ caption = models.CharField(_("caption"), max_length=200, blank=True)
+
+
+
+class ProgramPage(Page):
+ program = models.OneToOneField(
+ aircox.Program, models.CASCADE,
+ )
+
+
diff --git a/aircox_web/package.json b/aircox_web/package.json
new file mode 100644
index 0000000..f987767
--- /dev/null
+++ b/aircox_web/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "aircox-web-assets",
+ "version": "0.0.0",
+ "description": "Assets for Aircox Web",
+ "main": "index.js",
+ "author": "bkfox",
+ "license": "AGPL",
+ "devDependencies": {
+ "@fortawesome/fontawesome-free": "^5.8.2",
+ "mini-css-extract-plugin": "^0.5.0",
+ "css-loader": "^2.1.1",
+ "file-loader": "^3.0.1",
+ "ttf-loader": "^1.0.2",
+ "vue-loader": "^15.7.0",
+ "vue-style-loader": "^4.1.2",
+ "webpack": "^4.32.2",
+ "webpack-bundle-analyzer": "^3.3.2",
+ "webpack-bundle-tracker": "^0.4.2-beta",
+ "webpack-cli": "^3.3.2"
+ },
+ "dependencies": {
+ "bootstrap": "^4.3.1",
+ "buefy": "^0.7.8",
+ "vue": "^2.6.10"
+ }
+}
diff --git a/aircox_web/renderer.py b/aircox_web/renderer.py
new file mode 100644
index 0000000..19fcaba
--- /dev/null
+++ b/aircox_web/renderer.py
@@ -0,0 +1,20 @@
+from django.utils.html import format_html, mark_safe
+from feincms3.renderer import TemplatePluginRenderer
+
+from .models import Page, RichText, Image
+
+
+renderer = TemplatePluginRenderer()
+renderer.register_string_renderer(
+ RichText,
+ lambda plugin: mark_safe(plugin.text),
+)
+renderer.register_string_renderer(
+ Image,
+ lambda plugin: format_html(
+ '