create aircox_streamer as separate application
This commit is contained in:
parent
e30d1b54ef
commit
4e61ec1520
|
@ -1,8 +0,0 @@
|
||||||
from .article import ArticleAdmin
|
|
||||||
from .episode import DiffusionAdmin, EpisodeAdmin
|
|
||||||
from .log import LogAdmin
|
|
||||||
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
|
|
||||||
from .sound import SoundAdmin, TrackAdmin
|
|
||||||
from .station import StationAdmin
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import copy
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from ..models import Article
|
|
||||||
from .page import PageAdmin
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ArticleAdmin']
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Article)
|
|
||||||
class ArticleAdmin(PageAdmin):
|
|
||||||
list_filter = PageAdmin.list_filter
|
|
||||||
search_fields = PageAdmin.search_fields + ['parent__title']
|
|
||||||
# TODO: readonly field
|
|
||||||
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import copy
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
|
||||||
|
|
||||||
from aircox.models import Episode, Diffusion, Sound, Track
|
|
||||||
|
|
||||||
from .page import PageAdmin
|
|
||||||
from .sound import SoundInline, TracksInline
|
|
||||||
|
|
||||||
|
|
||||||
class DiffusionBaseAdmin:
|
|
||||||
fields = ['type', 'start', 'end']
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
fields = super().get_readonly_fields(request, obj)
|
|
||||||
if not request.user.has_perm('aircox_program.scheduling'):
|
|
||||||
fields += ['program', 'start', 'end']
|
|
||||||
return [field for field in fields if field in self.fields]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Diffusion)
|
|
||||||
class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
|
|
||||||
def start_date(self, obj):
|
|
||||||
return obj.local_start.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')
|
|
||||||
|
|
||||||
list_display = ('episode', 'start_date', 'end_date', 'type', 'initial')
|
|
||||||
list_filter = ('type', 'start', 'program')
|
|
||||||
list_editable = ('type',)
|
|
||||||
ordering = ('-start', 'id')
|
|
||||||
|
|
||||||
fields = ['type', 'start', 'end', 'initial', 'program']
|
|
||||||
|
|
||||||
|
|
||||||
class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
|
|
||||||
model = Diffusion
|
|
||||||
fk_name = 'episode'
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
|
||||||
return request.user.has_perm('aircox_program.scheduling')
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Episode)
|
|
||||||
class EpisodeAdmin(PageAdmin):
|
|
||||||
list_display = PageAdmin.list_display
|
|
||||||
list_filter = PageAdmin.list_filter
|
|
||||||
search_fields = PageAdmin.search_fields + ['parent__title']
|
|
||||||
# readonly_fields = ('parent',)
|
|
||||||
|
|
||||||
inlines = [TracksInline, SoundInline, DiffusionInline]
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from ..models import Log
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LogAdmin']
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Log)
|
|
||||||
class LogAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ['id', 'date', 'station', 'source', 'type', 'comment']
|
|
||||||
list_filter = ['date', 'source', 'station']
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from adminsortable2.admin import SortableInlineAdminMixin
|
|
||||||
|
|
||||||
from ..models import Category, Article, NavItem
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['CategoryAdmin', 'PageAdmin', 'NavItemInline']
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ['pk', 'title', 'slug']
|
|
||||||
list_editable = ['title', 'slug']
|
|
||||||
search_fields = ['title']
|
|
||||||
fields = ['title', 'slug']
|
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
|
||||||
|
|
||||||
|
|
||||||
# limit category choice
|
|
||||||
class PageAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('cover_thumb', 'title', 'status', 'category', 'parent')
|
|
||||||
list_display_links = ('cover_thumb', 'title')
|
|
||||||
list_editable = ('status', 'category')
|
|
||||||
list_filter = ('status', 'category')
|
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
|
||||||
|
|
||||||
search_fields = ['title', 'category__title']
|
|
||||||
fieldsets = [
|
|
||||||
('', {
|
|
||||||
'fields': ['title', 'slug', 'category', 'cover', 'content'],
|
|
||||||
}),
|
|
||||||
(_('Publication Settings'), {
|
|
||||||
'fields': ['featured', 'allow_comments', 'status', 'parent'],
|
|
||||||
'classes': ('collapse',),
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
change_form_template = 'admin/aircox/page_change_form.html'
|
|
||||||
|
|
||||||
def cover_thumb(self, obj):
|
|
||||||
return mark_safe('<img src="{}"/>'.format(obj.cover.icons['64'])) \
|
|
||||||
if obj.cover else ''
|
|
||||||
|
|
||||||
|
|
||||||
class NavItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
|
||||||
model = NavItem
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..models import Program, Schedule, Stream
|
|
||||||
from .page import PageAdmin
|
|
||||||
|
|
||||||
|
|
||||||
class ScheduleInline(admin.TabularInline):
|
|
||||||
model = Schedule
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
|
|
||||||
class StreamInline(admin.TabularInline):
|
|
||||||
fields = ['delay', 'begin', 'end']
|
|
||||||
model = Stream
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Program)
|
|
||||||
class ProgramAdmin(PageAdmin):
|
|
||||||
def schedule(self, obj):
|
|
||||||
return Schedule.objects.filter(program=obj).count() > 0
|
|
||||||
|
|
||||||
schedule.boolean = True
|
|
||||||
schedule.short_description = _("Schedule")
|
|
||||||
|
|
||||||
list_display = PageAdmin.list_display + ('schedule', 'station', 'active')
|
|
||||||
list_filter = PageAdmin.list_filter + ('station', 'active')
|
|
||||||
fieldsets = deepcopy(PageAdmin.fieldsets) + [
|
|
||||||
(_('Program Settings'), {
|
|
||||||
'fields': ['active', 'station', 'sync'],
|
|
||||||
'classes': ('collapse',),
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
|
||||||
search_fields = ['title']
|
|
||||||
|
|
||||||
inlines = [ScheduleInline, StreamInline]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Schedule)
|
|
||||||
class ScheduleAdmin(admin.ModelAdmin):
|
|
||||||
def program_title(self, obj):
|
|
||||||
return obj.program.title
|
|
||||||
program_title.short_description = _('Program')
|
|
||||||
|
|
||||||
def freq(self, obj):
|
|
||||||
return obj.get_frequency_verbose()
|
|
||||||
freq.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 = ['program_title', 'freq', 'time', 'timezone', 'duration',
|
|
||||||
'rerun']
|
|
||||||
list_editable = ['time', 'duration']
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
if obj:
|
|
||||||
return ['program', 'date', 'frequency']
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Stream)
|
|
||||||
class StreamAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('id', 'program', 'delay', 'begin', 'end')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
|
||||||
|
|
||||||
from adminsortable2.admin import SortableInlineAdminMixin
|
|
||||||
|
|
||||||
from ..models import Sound, 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']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SoundInline(admin.TabularInline):
|
|
||||||
model = Sound
|
|
||||||
fields = ['type', 'path', 'embed', 'duration', 'is_public']
|
|
||||||
readonly_fields = ['type', 'path', 'duration']
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Sound)
|
|
||||||
class SoundAdmin(admin.ModelAdmin):
|
|
||||||
def filename(self, obj):
|
|
||||||
return '/'.join(obj.path.split('/')[-2:])
|
|
||||||
filename.short_description=_('file')
|
|
||||||
|
|
||||||
fields = None
|
|
||||||
list_display = ['id', 'name', 'program', 'type', 'duration',
|
|
||||||
'is_public', 'is_good_quality', 'episode', 'filename']
|
|
||||||
list_filter = ('program', 'type', 'is_good_quality', 'is_public')
|
|
||||||
|
|
||||||
search_fields = ['name', 'program']
|
|
||||||
fieldsets = [
|
|
||||||
(None, {'fields': ['name', 'path', 'type', 'program', 'episode']}),
|
|
||||||
(None, {'fields': ['embed', 'duration', 'is_public', 'mtime']}),
|
|
||||||
(None, {'fields': ['is_good_quality']})
|
|
||||||
]
|
|
||||||
readonly_fields = ('path', 'duration',)
|
|
||||||
inlines = [TracksInline]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Track)
|
|
||||||
class TrackAdmin(admin.ModelAdmin):
|
|
||||||
def tag_list(self, obj):
|
|
||||||
return u", ".join(o.name for o in obj.tags.all())
|
|
||||||
|
|
||||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'timestamp']
|
|
||||||
list_editable = ['artist', 'title']
|
|
||||||
list_filter = ['artist', 'title', 'tags']
|
|
||||||
|
|
||||||
search_fields = ['artist', 'title']
|
|
||||||
fieldsets = [
|
|
||||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
|
|
||||||
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
|
||||||
]
|
|
||||||
|
|
||||||
# TODO on edit: readonly_fields = ['episode', 'sound']
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from ..models import Port, Station
|
|
||||||
from .page import NavItemInline
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['PortInline', 'StationAdmin']
|
|
||||||
|
|
||||||
|
|
||||||
class PortInline(admin.StackedInline):
|
|
||||||
model = Port
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Station)
|
|
||||||
class StationAdmin(admin.ModelAdmin):
|
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
|
||||||
inlines = [PortInline, NavItemInline]
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ class AircoxConfig(AppConfig):
|
||||||
|
|
||||||
|
|
||||||
class AircoxAdminConfig(AdminConfig):
|
class AircoxAdminConfig(AdminConfig):
|
||||||
default_site = 'aircox.views.admin.AdminSite'
|
default_site = 'aircox.admin_site.AdminSite'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .program import Program, Stream, Schedule
|
||||||
from .episode import Episode, Diffusion
|
from .episode import Episode, Diffusion
|
||||||
from .log import Log
|
from .log import Log
|
||||||
from .sound import Sound, Track
|
from .sound import Sound, Track
|
||||||
from .station import Station, Port
|
from .station import Station
|
||||||
|
|
||||||
from . import signals
|
from . import signals
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -44,8 +44,8 @@ def program_post_save(sender, instance, created, *args, **kwargs):
|
||||||
Clean-up later diffusions when a program becomes inactive
|
Clean-up later diffusions when a program becomes inactive
|
||||||
"""
|
"""
|
||||||
if not instance.active:
|
if not instance.active:
|
||||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
Diffusion.object.program(instance).after(tz.now()).delete()
|
||||||
Episode.object.program(instance).filter(diffusion__isnull=True) \
|
Episode.object.parent(instance).filter(diffusion__isnull=True) \
|
||||||
.delete()
|
.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,7 +94,6 @@ def schedule_pre_delete(sender, instance, *args, **kwargs):
|
||||||
@receiver(signals.post_delete, sender=Diffusion)
|
@receiver(signals.post_delete, sender=Diffusion)
|
||||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
||||||
Episode.objects.filter(diffusion__isnull=True, content__isnull=True,
|
Episode.objects.filter(diffusion__isnull=True, content__isnull=True,
|
||||||
sound__isnull=True) \
|
sound__isnull=True).delete()
|
||||||
.delete()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
|
@ -9,7 +8,7 @@ from filer.fields.image import FilerImageField
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Station', 'StationQuerySet', 'Port']
|
__all__ = ['Station', 'StationQuerySet']
|
||||||
|
|
||||||
|
|
||||||
class StationQuerySet(models.QuerySet):
|
class StationQuerySet(models.QuerySet):
|
||||||
|
@ -22,6 +21,9 @@ class StationQuerySet(models.QuerySet):
|
||||||
return self.order_by('-default', 'pk').first()
|
return self.order_by('-default', 'pk').first()
|
||||||
return self.filter(pk=station).first()
|
return self.filter(pk=station).first()
|
||||||
|
|
||||||
|
def active(self):
|
||||||
|
return self.filter(active=True)
|
||||||
|
|
||||||
|
|
||||||
class Station(models.Model):
|
class Station(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -44,7 +46,12 @@ class Station(models.Model):
|
||||||
default = models.BooleanField(
|
default = models.BooleanField(
|
||||||
_('default station'),
|
_('default station'),
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_('if checked, this station is used as the main one')
|
help_text=_('use this station as the main one.')
|
||||||
|
)
|
||||||
|
active = models.BooleanField(
|
||||||
|
_('active'),
|
||||||
|
default=True,
|
||||||
|
help_text=_('whether this station is still active or not.')
|
||||||
)
|
)
|
||||||
logo = FilerImageField(
|
logo = FilerImageField(
|
||||||
on_delete=models.SET_NULL, null=True, blank=True,
|
on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
|
@ -79,6 +86,20 @@ class Station(models.Model):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PortQuerySet(models.QuerySet):
|
||||||
|
def active(self, value=True):
|
||||||
|
""" Active ports """
|
||||||
|
return self.filter(active=value)
|
||||||
|
|
||||||
|
def output(self):
|
||||||
|
""" Filter in output ports """
|
||||||
|
return self.filter(direction=Port.DIRECTION_OUTPUT)
|
||||||
|
|
||||||
|
def input(self):
|
||||||
|
""" Fitler in input ports """
|
||||||
|
return self.filter(direction=Port.DIRECTION_INPUT)
|
||||||
|
|
||||||
|
|
||||||
class Port(models.Model):
|
class Port(models.Model):
|
||||||
"""
|
"""
|
||||||
Represent an audio input/output for the audio stream
|
Represent an audio input/output for the audio stream
|
||||||
|
@ -126,6 +147,14 @@ class Port(models.Model):
|
||||||
blank=True, null=True
|
blank=True, null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = PortQuerySet.as_manager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{direction}: {type} #{id}".format(
|
||||||
|
direction=self.get_direction_display(),
|
||||||
|
type=self.get_type_display(), id=self.pk or ''
|
||||||
|
)
|
||||||
|
|
||||||
def is_valid_type(self):
|
def is_valid_type(self):
|
||||||
"""
|
"""
|
||||||
Return True if the type is available for the given direction.
|
Return True if the type is available for the given direction.
|
||||||
|
@ -148,11 +177,3 @@ class Port(models.Model):
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{direction}: {type} #{id}".format(
|
|
||||||
direction=self.get_direction_display(),
|
|
||||||
type=self.get_type_display(),
|
|
||||||
id=self.pk or ''
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# Code inspired from rest_framework of course.
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
|
||||||
|
|
|
@ -7221,7 +7221,7 @@ a.navbar-item.is-active {
|
||||||
.card-super-title {
|
.card-super-title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: 1.2em;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
|
|
|
@ -234,7 +234,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! buefy/dist/buefy.css */ \"./node_modules/buefy/dist/buefy.css\");\n/* harmony import */ var buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_7__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
|
|
@ -7203,7 +7203,7 @@ a.navbar-item.is-active {
|
||||||
.card-super-title {
|
.card-super-title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: 1.2em;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
|
|
|
@ -175,7 +175,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! buefy/dist/buefy.css */ \"./node_modules/buefy/dist/buefy.css\");\n/* harmony import */ var buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(buefy_dist_buefy_css__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_7__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,17 +22,6 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./node
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ "./node_modules/buefy/dist/buefy.css":
|
|
||||||
/*!*******************************************!*\
|
|
||||||
!*** ./node_modules/buefy/dist/buefy.css ***!
|
|
||||||
\*******************************************/
|
|
||||||
/*! no static exports found */
|
|
||||||
/***/ (function(module, exports, __webpack_require__) {
|
|
||||||
|
|
||||||
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./node_modules/buefy/dist/buefy.css?");
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
|
|
||||||
/***/ "./node_modules/process/browser.js":
|
/***/ "./node_modules/process/browser.js":
|
||||||
/*!*****************************************!*\
|
/*!*****************************************!*\
|
||||||
!*** ./node_modules/process/browser.js ***!
|
!*** ./node_modules/process/browser.js ***!
|
||||||
|
|
|
@ -2,16 +2,9 @@
|
||||||
{% comment %}
|
{% comment %}
|
||||||
Context:
|
Context:
|
||||||
-
|
-
|
||||||
-
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
- sidebar:
|
|
||||||
- logs
|
|
||||||
- diffusions
|
|
||||||
- main:
|
- main:
|
||||||
- focused
|
- focused
|
||||||
- nav to 'publications' view
|
- nav to 'publications' view
|
||||||
-
|
|
||||||
|
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
@ -24,11 +17,17 @@ TODO:
|
||||||
{% for object in top_diffs %}
|
{% for object in top_diffs %}
|
||||||
{% with is_primary=object.is_now %}
|
{% with is_primary=object.is_now %}
|
||||||
<div class="column is-relative">
|
<div class="column is-relative">
|
||||||
<h4 class="card-super-title">
|
<h4 class="card-super-title" title="{{ object.start }}">
|
||||||
{% if is_primary %}
|
{% if is_primary %}
|
||||||
<span class="fas fa-play"></span>
|
<span class="fas fa-play"></span>
|
||||||
{% trans "Currently" %}
|
<time datetime="{{ object.start }}">
|
||||||
{% else %}{{ object.start|date:"H:i" }}{% endif %}
|
{% trans "Currently" %}
|
||||||
|
{% else %}{{ object.start|date:"H:i" }}{% endif %}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
{% if object.episode.category %}
|
||||||
|
// {{ object.episode.category.title }}
|
||||||
|
{% endif %}
|
||||||
</h4>
|
</h4>
|
||||||
{% include object.item_template_name %}
|
{% include object.item_template_name %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import include, path, register_converter
|
from django.urls import include, path, register_converter
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
"""
|
|
||||||
Aircox admin tools and views.
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, \
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
PermissionRequiredMixin, UserPassesTestMixin
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from ..models import Program
|
|
||||||
from .log import LogListView
|
from .log import LogListView
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['BaseAdminView', 'StatisticsView']
|
||||||
|
|
||||||
|
|
||||||
class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
||||||
title = ''
|
title = ''
|
||||||
|
|
||||||
|
@ -27,6 +23,7 @@ class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
||||||
|
|
||||||
class StatisticsView(BaseAdminView, LogListView, ListView):
|
class StatisticsView(BaseAdminView, LogListView, ListView):
|
||||||
template_name = 'admin/aircox/statistics.html'
|
template_name = 'admin/aircox/statistics.html'
|
||||||
|
redirect_date_url = 'tools-stats'
|
||||||
title = _('Statistics')
|
title = _('Statistics')
|
||||||
date = None
|
date = None
|
||||||
|
|
||||||
|
@ -34,26 +31,3 @@ class StatisticsView(BaseAdminView, LogListView, ListView):
|
||||||
return super().get_object_list(logs, True)
|
return super().get_object_list(logs, True)
|
||||||
|
|
||||||
|
|
||||||
class AdminSite(admin.AdminSite):
|
|
||||||
def each_context(self, request):
|
|
||||||
context = super().each_context(request)
|
|
||||||
context.update({
|
|
||||||
'programs': Program.objects.all().active().values('pk', 'title'),
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_urls(self):
|
|
||||||
from django.urls import path, include
|
|
||||||
urls = super().get_urls() + [
|
|
||||||
path('tools/statistics/',
|
|
||||||
self.admin_view(StatisticsView.as_view()),
|
|
||||||
name='tools-stats'),
|
|
||||||
path('tools/statistics/<date:date>/',
|
|
||||||
self.admin_view(StatisticsView.as_view()),
|
|
||||||
name='tools-stats'),
|
|
||||||
]
|
|
||||||
return urls
|
|
||||||
|
|
||||||
|
|
||||||
admin_site = AdminSite()
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import datetime
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from rest_framework.generics import ListAPIView
|
from rest_framework.generics import ListAPIView
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from ..models import Log
|
from ..models import Log
|
||||||
from ..serializers import LogInfo, LogInfoSerializer
|
from ..serializers import LogInfo, LogInfoSerializer
|
||||||
|
@ -44,4 +46,3 @@ class LogListAPIView(LogListMixin, BaseAPIView, ListAPIView):
|
||||||
return super().get_serializer(self.get_object_list(queryset, full),
|
return super().get_serializer(self.get_object_list(queryset, full),
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,11 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||||
kwargs['sidebar_object_list'] = sidebar_object_list[:self.list_count]
|
kwargs['sidebar_object_list'] = sidebar_object_list[:self.list_count]
|
||||||
kwargs['sidebar_list_url'] = self.get_sidebar_url()
|
kwargs['sidebar_list_url'] = self.get_sidebar_url()
|
||||||
|
|
||||||
if not 'audio_streams' in kwargs:
|
if 'audio_streams' not in kwargs:
|
||||||
streams = self.station.audio_streams
|
streams = self.station.audio_streams
|
||||||
streams = streams and streams.split('\n')
|
streams = streams and streams.split('\n')
|
||||||
kwargs['audio_streams'] = streams
|
kwargs['audio_streams'] = streams
|
||||||
|
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ from .base import BaseView
|
||||||
__all__ = ['PageDetailView', 'PageListView']
|
__all__ = ['PageDetailView', 'PageListView']
|
||||||
|
|
||||||
|
|
||||||
# TODO: pagination: in template, only a limited number of pages displayed
|
|
||||||
class PageListView(BaseView, ListView):
|
class PageListView(BaseView, ListView):
|
||||||
template_name = 'aircox/page_list.html'
|
template_name = 'aircox/page_list.html'
|
||||||
item_template_name = 'aircox/widgets/page_item.html'
|
item_template_name = 'aircox/widgets/page_item.html'
|
||||||
|
|
0
aircox_streamer/__init__.py
Normal file
0
aircox_streamer/__init__.py
Normal file
17
aircox_streamer/admin.py
Normal file
17
aircox_streamer/admin.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from aircox.admin import StationAdmin
|
||||||
|
from .models import Port
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['PortInline']
|
||||||
|
|
||||||
|
|
||||||
|
class PortInline(admin.StackedInline):
|
||||||
|
model = Port
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
StationAdmin.inlines = (PortInline,) + StationAdmin.inlines
|
||||||
|
|
||||||
|
|
5
aircox_streamer/apps.py
Normal file
5
aircox_streamer/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AircoxStreamerConfig(AppConfig):
|
||||||
|
name = 'aircox_streamer'
|
|
@ -11,11 +11,19 @@ import tzlocal
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from . import settings
|
from aircox import settings
|
||||||
from .models import Port, Station, Sound
|
from aircox.models import Station, Sound
|
||||||
from .connector import Connector
|
from aircox.utils import to_seconds
|
||||||
from .utils import to_seconds
|
|
||||||
|
|
||||||
|
from .connector import Connector
|
||||||
|
from .models import Port
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['BaseMetadata', 'Request', 'Streamer', 'Source',
|
||||||
|
'PlaylistSource', 'QueueSource']
|
||||||
|
|
||||||
|
# TODO: for the moment, update in station and program names do not update the
|
||||||
|
# related fields.
|
||||||
|
|
||||||
# FIXME liquidsoap does not manage timezones -- we have to convert
|
# FIXME liquidsoap does not manage timezones -- we have to convert
|
||||||
# 'on_air' metadata we get from it into utc one in order to work
|
# 'on_air' metadata we get from it into utc one in order to work
|
||||||
|
@ -25,12 +33,64 @@ local_tz = tzlocal.get_localzone()
|
||||||
logger = logging.getLogger('aircox')
|
logger = logging.getLogger('aircox')
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMetadata:
|
||||||
|
""" Base class for handling request metadata. """
|
||||||
|
controller = None
|
||||||
|
""" Controller """
|
||||||
|
rid = None
|
||||||
|
""" Request id """
|
||||||
|
uri = None
|
||||||
|
""" Request uri """
|
||||||
|
status = None
|
||||||
|
""" Current playing status """
|
||||||
|
air_time = None
|
||||||
|
""" Launch datetime """
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, controller=None, rid=None, data=None):
|
||||||
|
self.controller = controller
|
||||||
|
self.rid = rid
|
||||||
|
if data is not None:
|
||||||
|
self.validate(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_playing(self):
|
||||||
|
return self.status == 'playing'
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
data = self.controller.set('request.metadata ', self.rid, parse=True)
|
||||||
|
if data:
|
||||||
|
self.validate(data)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""
|
||||||
|
Validate provided data and set as attribute (must already be
|
||||||
|
declared)
|
||||||
|
"""
|
||||||
|
for key, value in data.items():
|
||||||
|
if hasattr(self, key) and not callable(getattr(self, key)):
|
||||||
|
setattr(self, key, value)
|
||||||
|
self.uri = data.get('initial_uri')
|
||||||
|
|
||||||
|
air_time = data.get('on_air')
|
||||||
|
if air_time:
|
||||||
|
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
||||||
|
self.air_time = local_tz.localize(air_time)
|
||||||
|
else:
|
||||||
|
self.air_time = None
|
||||||
|
|
||||||
|
|
||||||
|
class Request(BaseMetadata):
|
||||||
|
title = None
|
||||||
|
artist = None
|
||||||
|
|
||||||
|
|
||||||
class Streamer:
|
class Streamer:
|
||||||
connector = None
|
connector = None
|
||||||
process = None
|
process = None
|
||||||
|
|
||||||
station = None
|
station = None
|
||||||
template_name = 'aircox/scripts/station.liq'
|
template_name = 'aircox_streamer/scripts/station.liq'
|
||||||
path = None
|
path = None
|
||||||
""" Config path """
|
""" Config path """
|
||||||
sources = None
|
sources = None
|
||||||
|
@ -41,9 +101,16 @@ class Streamer:
|
||||||
# moment
|
# moment
|
||||||
# on_air = None
|
# on_air = None
|
||||||
# """ On-air request ids (rid) """
|
# """ On-air request ids (rid) """
|
||||||
|
inputs = None
|
||||||
|
""" Queryset to input ports """
|
||||||
|
outputs = None
|
||||||
|
""" Queryset to output ports """
|
||||||
|
|
||||||
def __init__(self, station):
|
def __init__(self, station, connector=None):
|
||||||
self.station = station
|
self.station = station
|
||||||
|
self.inputs = self.station.port_set.active().input()
|
||||||
|
self.outputs = self.station.port_set.active().output()
|
||||||
|
|
||||||
self.id = self.station.slug.replace('-', '_')
|
self.id = self.station.slug.replace('-', '_')
|
||||||
self.path = os.path.join(station.path, 'station.liq')
|
self.path = os.path.join(station.path, 'station.liq')
|
||||||
self.connector = Connector(os.path.join(station.path, 'station.sock'))
|
self.connector = Connector(os.path.join(station.path, 'station.sock'))
|
||||||
|
@ -63,6 +130,7 @@ class Streamer:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
|
""" True if holds a running process """
|
||||||
if self.process is None:
|
if self.process is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -74,19 +142,6 @@ class Streamer:
|
||||||
logger.debug('process died with return code %s' % returncode)
|
logger.debug('process died with return code %s' % returncode)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIXME: is it really needed as property?
|
|
||||||
@property
|
|
||||||
def inputs(self):
|
|
||||||
""" Return input ports of the station """
|
|
||||||
return self.station.port_set.filter(direction=Port.DIRECTION_INPUT,
|
|
||||||
active=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outputs(self):
|
|
||||||
""" Return output ports of the station """
|
|
||||||
return self.station.port_set.filter(direction=Port.DIRECTION_OUTPUT,
|
|
||||||
active=True)
|
|
||||||
|
|
||||||
# Sources and config ###############################################
|
# Sources and config ###############################################
|
||||||
def send(self, *args, **kwargs):
|
def send(self, *args, **kwargs):
|
||||||
return self.connector.send(*args, **kwargs) or ''
|
return self.connector.send(*args, **kwargs) or ''
|
||||||
|
@ -121,9 +176,6 @@ class Streamer:
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
""" Fetch data from liquidsoap """
|
""" Fetch data from liquidsoap """
|
||||||
if self.process is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
source.fetch()
|
source.fetch()
|
||||||
|
|
||||||
|
@ -182,19 +234,11 @@ class Streamer:
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
|
|
||||||
class Source:
|
class Source(BaseMetadata):
|
||||||
controller = None
|
controller = None
|
||||||
""" parent controller """
|
""" parent controller """
|
||||||
id = None
|
id = None
|
||||||
""" source id """
|
""" source id """
|
||||||
uri = ''
|
|
||||||
""" source uri """
|
|
||||||
rid = None
|
|
||||||
""" request id """
|
|
||||||
air_time = None
|
|
||||||
""" on air time """
|
|
||||||
status = None
|
|
||||||
""" source status """
|
|
||||||
remaining = 0.0
|
remaining = 0.0
|
||||||
""" remaining time """
|
""" remaining time """
|
||||||
|
|
||||||
|
@ -202,16 +246,12 @@ class Source:
|
||||||
def station(self):
|
def station(self):
|
||||||
return self.controller.station
|
return self.controller.station
|
||||||
|
|
||||||
@property
|
|
||||||
def is_playing(self):
|
|
||||||
return self.status == 'playing'
|
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
# def is_on_air(self):
|
# def is_on_air(self):
|
||||||
# return self.rid is not None and self.rid in self.controller.on_air
|
# return self.rid is not None and self.rid in self.controller.on_air
|
||||||
|
|
||||||
def __init__(self, controller, id=None):
|
def __init__(self, controller=None, id=None, *args, **kwargs):
|
||||||
self.controller = controller
|
super().__init__(controller, *args, **kwargs)
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
|
@ -219,23 +259,12 @@ class Source:
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
data = self.controller.send(self.id, '.remaining')
|
data = self.controller.send(self.id, '.remaining')
|
||||||
self.remaining = float(data)
|
if data:
|
||||||
|
self.remaining = float(data)
|
||||||
|
|
||||||
data = self.controller.send(self.id, '.get', parse=True)
|
data = self.controller.send(self.id, '.get', parse=True)
|
||||||
self.on_metadata(data if data and isinstance(data, dict) else {})
|
if data:
|
||||||
|
self.validate(data if data and isinstance(data, dict) else {})
|
||||||
def on_metadata(self, data):
|
|
||||||
""" Update source info from provided request metadata """
|
|
||||||
self.rid = data.get('rid') or None
|
|
||||||
self.uri = data.get('initial_uri') or None
|
|
||||||
self.status = data.get('status') or None
|
|
||||||
|
|
||||||
air_time = data.get('on_air')
|
|
||||||
if air_time:
|
|
||||||
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
|
||||||
self.air_time = local_tz.localize(air_time)
|
|
||||||
else:
|
|
||||||
self.air_time = None
|
|
||||||
|
|
||||||
def skip(self):
|
def skip(self):
|
||||||
""" Skip the current source sound """
|
""" Skip the current source sound """
|
||||||
|
@ -271,15 +300,15 @@ class PlaylistSource(Source):
|
||||||
""" Get playlist's sounds queryset """
|
""" Get playlist's sounds queryset """
|
||||||
return self.program.sound_set.archive()
|
return self.program.sound_set.archive()
|
||||||
|
|
||||||
def load_playlist(self):
|
def get_playlist(self):
|
||||||
""" Load playlist """
|
""" Get playlist from db """
|
||||||
self.playlist = self.get_sound_queryset().paths()
|
return self.get_sound_queryset().paths()
|
||||||
|
|
||||||
def write_playlist(self):
|
def write_playlist(self, playlist=[]):
|
||||||
""" Write playlist file. """
|
""" Write playlist to file. """
|
||||||
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
||||||
with open(self.path, 'w') as file:
|
with open(self.path, 'w') as file:
|
||||||
file.write('\n'.join(self.playlist or []))
|
file.write('\n'.join(playlist or []))
|
||||||
|
|
||||||
def stream(self):
|
def stream(self):
|
||||||
""" Return program's stream info if any (or None) as dict. """
|
""" Return program's stream info if any (or None) as dict. """
|
||||||
|
@ -296,15 +325,21 @@ class PlaylistSource(Source):
|
||||||
}
|
}
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
self.load_playlist()
|
playlist = self.get_playlist()
|
||||||
self.write_playlist()
|
self.write_playlist(playlist)
|
||||||
|
|
||||||
|
|
||||||
class QueueSource(Source):
|
class QueueSource(Source):
|
||||||
queue = None
|
queue = None
|
||||||
""" Source's queue (excluded on_air request) """
|
""" Source's queue (excluded on_air request) """
|
||||||
|
as_requests = False
|
||||||
|
""" If True, queue is a list of Request """
|
||||||
|
|
||||||
def append(self, *paths):
|
def __init__(self, *args, queue_metadata=False, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.queue_metadata = queue_metadata
|
||||||
|
|
||||||
|
def push(self, *paths):
|
||||||
""" Add the provided paths to source's play queue """
|
""" Add the provided paths to source's play queue """
|
||||||
for path in paths:
|
for path in paths:
|
||||||
self.controller.send(self.id, '_queue.push ', path)
|
self.controller.send(self.id, '_queue.push ', path)
|
||||||
|
@ -312,4 +347,12 @@ class QueueSource(Source):
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
super().fetch()
|
super().fetch()
|
||||||
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
||||||
self.queue = queue
|
if not self.as_requests:
|
||||||
|
self.queue = queue
|
||||||
|
return
|
||||||
|
|
||||||
|
self.queue = [Request(self.controller, rid) for rid in queue]
|
||||||
|
for request in self.queue:
|
||||||
|
request.fetch()
|
||||||
|
|
||||||
|
|
0
aircox_streamer/management/__init__.py
Executable file
0
aircox_streamer/management/__init__.py
Executable file
0
aircox_streamer/management/commands/__init__.py
Executable file
0
aircox_streamer/management/commands/__init__.py
Executable file
|
@ -21,10 +21,11 @@ from django.db.models import Q
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from aircox.controllers import Streamer, PlaylistSource
|
|
||||||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||||
from aircox.utils import date_range
|
from aircox.utils import date_range
|
||||||
|
|
||||||
|
from aircox_streamer.liquidsoap import Streamer, PlaylistSource
|
||||||
|
|
||||||
|
|
||||||
# force using UTC
|
# force using UTC
|
||||||
tz.activate(pytz.UTC)
|
tz.activate(pytz.UTC)
|
||||||
|
@ -227,7 +228,7 @@ class Monitor:
|
||||||
|
|
||||||
def start_diff(self, source, diff):
|
def start_diff(self, source, diff):
|
||||||
playlist = Sound.objects.episode(id=diff.episode_id).paths()
|
playlist = Sound.objects.episode(id=diff.episode_id).paths()
|
||||||
source.append(*playlist)
|
source.push(*playlist)
|
||||||
self.log(type=Log.TYPE_START, source=source.id, diffusion=diff,
|
self.log(type=Log.TYPE_START, source=source.id, diffusion=diff,
|
||||||
comment=str(diff))
|
comment=str(diff))
|
||||||
|
|
101
aircox_streamer/models.py
Normal file
101
aircox_streamer/models.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from aircox.models import Station
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['PortQuerySet', 'Port']
|
||||||
|
|
||||||
|
|
||||||
|
class PortQuerySet(models.QuerySet):
|
||||||
|
def active(self, value=True):
|
||||||
|
""" Active ports """
|
||||||
|
return self.filter(active=value)
|
||||||
|
|
||||||
|
def output(self):
|
||||||
|
""" Filter in output ports """
|
||||||
|
return self.filter(direction=Port.DIRECTION_OUTPUT)
|
||||||
|
|
||||||
|
def input(self):
|
||||||
|
""" Fitler in input ports """
|
||||||
|
return self.filter(direction=Port.DIRECTION_INPUT)
|
||||||
|
|
||||||
|
|
||||||
|
class Port(models.Model):
|
||||||
|
"""
|
||||||
|
Represent an audio input/output for the audio stream
|
||||||
|
generation.
|
||||||
|
|
||||||
|
You might want to take a look to LiquidSoap's documentation
|
||||||
|
for the options available for each kind of input/output.
|
||||||
|
|
||||||
|
Some port types may be not available depending on the
|
||||||
|
direction of the port.
|
||||||
|
"""
|
||||||
|
DIRECTION_INPUT = 0x00
|
||||||
|
DIRECTION_OUTPUT = 0x01
|
||||||
|
DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')),
|
||||||
|
(DIRECTION_OUTPUT, _('output')))
|
||||||
|
|
||||||
|
TYPE_JACK = 0x00
|
||||||
|
TYPE_ALSA = 0x01
|
||||||
|
TYPE_PULSEAUDIO = 0x02
|
||||||
|
TYPE_ICECAST = 0x03
|
||||||
|
TYPE_HTTP = 0x04
|
||||||
|
TYPE_HTTPS = 0x05
|
||||||
|
TYPE_FILE = 0x06
|
||||||
|
TYPE_CHOICES = (
|
||||||
|
# display value are not translated becaused used as is in config
|
||||||
|
(TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'),
|
||||||
|
(TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'),
|
||||||
|
(TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'),
|
||||||
|
(TYPE_FILE, 'file')
|
||||||
|
)
|
||||||
|
|
||||||
|
station = models.ForeignKey(
|
||||||
|
Station, models.CASCADE, verbose_name=_('station'), related_name='+')
|
||||||
|
direction = models.SmallIntegerField(
|
||||||
|
_('direction'), choices=DIRECTION_CHOICES)
|
||||||
|
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
|
||||||
|
active = models.BooleanField(
|
||||||
|
_('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
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = PortQuerySet.as_manager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{direction}: {type} #{id}".format(
|
||||||
|
direction=self.get_direction_display(),
|
||||||
|
type=self.get_type_display(), id=self.pk or ''
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_valid_type(self):
|
||||||
|
"""
|
||||||
|
Return True if the type is available for the given direction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.direction == self.DIRECTION_INPUT:
|
||||||
|
return self.type not in (
|
||||||
|
self.TYPE_ICECAST, self.TYPE_FILE
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.type not in (
|
||||||
|
self.TYPE_HTTP, self.TYPE_HTTPS
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.is_valid_type():
|
||||||
|
raise ValueError(
|
||||||
|
"port type is not allowed with the given port direction"
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
40
aircox_streamer/serializers.py
Normal file
40
aircox_streamer/serializers.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['RequestSerializer', 'StreamerSerializer', 'SourceSerializer',
|
||||||
|
'PlaylistSerializer', 'QueueSourceSerializer']
|
||||||
|
# TODO: use models' serializers
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMetadataSerializer(serializers.Serializer):
|
||||||
|
rid = serializers.IntegerField()
|
||||||
|
air_time = serializers.DateTimeField()
|
||||||
|
uri = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class RequestSerializer(serializers.Serializer):
|
||||||
|
title = serializers.CharField()
|
||||||
|
artist = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class StreamerSerializer(serializers.Serializer):
|
||||||
|
station = serializers.CharField(source='station.title')
|
||||||
|
|
||||||
|
|
||||||
|
class SourceSerializer(BaseMetadataSerializer):
|
||||||
|
id = serializers.CharField()
|
||||||
|
uri = serializers.CharField()
|
||||||
|
rid = serializers.IntegerField()
|
||||||
|
air_time = serializers.DateTimeField()
|
||||||
|
status = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistSerializer(SourceSerializer):
|
||||||
|
program = serializers.CharField(source='program.title')
|
||||||
|
playlist = serializers.ListField(child=serializers.CharField())
|
||||||
|
|
||||||
|
|
||||||
|
class QueueSourceSerializer(SourceSerializer):
|
||||||
|
queue = serializers.ListField(child=RequestSerializer())
|
||||||
|
|
||||||
|
|
3
aircox_streamer/tests.py
Normal file
3
aircox_streamer/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
aircox_streamer/views.py
Normal file
3
aircox_streamer/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
158
aircox_streamer/viewsets.py
Normal file
158
aircox_streamer/viewsets.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
from django.http import Http404
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
|
||||||
|
from aircox import controllers
|
||||||
|
from aircox.models import Station
|
||||||
|
from .serializers import *
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['Streamers', 'BaseControllerAPIView',
|
||||||
|
'RequestViewSet', 'StreamerViewSet', 'SourceViewSet',
|
||||||
|
'PlaylistSourceViewSet', 'QueueSourceViewSet']
|
||||||
|
|
||||||
|
|
||||||
|
class Streamers:
|
||||||
|
date = None
|
||||||
|
""" next update datetime """
|
||||||
|
streamers = None
|
||||||
|
""" stations by station id """
|
||||||
|
timeout = None
|
||||||
|
""" timedelta to next update """
|
||||||
|
|
||||||
|
def __init__(self, timeout=None):
|
||||||
|
self.timeout = timeout or tz.timedelta(seconds=2)
|
||||||
|
|
||||||
|
def load(self, force=False):
|
||||||
|
# FIXME: cf. TODO in aircox.controllers about model updates
|
||||||
|
stations = Station.objects.active()
|
||||||
|
if self.streamers is None or force:
|
||||||
|
self.streamers = {station.pk: controllers.Streamer(station)
|
||||||
|
for station in stations}
|
||||||
|
return
|
||||||
|
|
||||||
|
streamers = self.streamers
|
||||||
|
self.streamers = {station.pk: controllers.Streamer(station)
|
||||||
|
if station.pk in streamers else streamers[station.pk]
|
||||||
|
for station in stations}
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
if self.streamers is None:
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
now = tz.now()
|
||||||
|
if self.date is not None and now < self.date:
|
||||||
|
return
|
||||||
|
|
||||||
|
for streamer in self.streamers.values():
|
||||||
|
streamer.fetch()
|
||||||
|
self.date = now + self.timeout
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
self.fetch()
|
||||||
|
return self.streamers.get(key, default)
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
self.fetch()
|
||||||
|
return self.streamers.values()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.streamers[key]
|
||||||
|
|
||||||
|
|
||||||
|
streamers = Streamers()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseControllerAPIView(viewsets.ViewSet):
|
||||||
|
permission_classes = (IsAdminUser,)
|
||||||
|
serializer = None
|
||||||
|
streamer = None
|
||||||
|
|
||||||
|
def get_streamer(self, pk=None):
|
||||||
|
streamer = streamers.get(self.request.pk if pk is None else pk)
|
||||||
|
if not streamer:
|
||||||
|
raise Http404('station not found')
|
||||||
|
return streamer
|
||||||
|
|
||||||
|
def get_serializer(self, obj, **kwargs):
|
||||||
|
return self.serializer(obj, **kwargs)
|
||||||
|
|
||||||
|
def serialize(self, obj, **kwargs):
|
||||||
|
serializer = self.get_serializer(obj, **kwargs)
|
||||||
|
return serializer.data
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.streamer = self.get_streamer(request.station.pk)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestViewSet(BaseControllerAPIView):
|
||||||
|
serializer = RequestSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StreamerViewSet(BaseControllerAPIView):
|
||||||
|
serializer = StreamerSerializer
|
||||||
|
|
||||||
|
def retrieve(self, request, pk=None):
|
||||||
|
return self.serialize(self.streamer)
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
return self.serialize(streamers.values(), many=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceViewSet(BaseControllerAPIView):
|
||||||
|
serializer = SourceSerializer
|
||||||
|
model = controllers.Source
|
||||||
|
|
||||||
|
def get_sources(self):
|
||||||
|
return (s for s in self.streamer.souces if isinstance(s, self.model))
|
||||||
|
|
||||||
|
def get_source(self, pk):
|
||||||
|
source = next((source for source in self.get_sources()
|
||||||
|
if source.pk == pk), None)
|
||||||
|
if source is None:
|
||||||
|
raise Http404('source `%s` not found' % pk)
|
||||||
|
return source
|
||||||
|
|
||||||
|
def retrieve(self, request, pk=None):
|
||||||
|
source = self.get_source(pk)
|
||||||
|
return self.serialize(source)
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
return self.serialize(self.get_sources(), many=True)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def sync(self, request, pk):
|
||||||
|
self.get_source(pk).sync()
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def skip(self, request, pk):
|
||||||
|
self.get_source(pk).skip()
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def restart(self, request, pk):
|
||||||
|
self.get_source(pk).restart()
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def seek(self, request, pk):
|
||||||
|
count = request.POST['seek']
|
||||||
|
self.get_source(pk).seek(count)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistSourceViewSet(SourceViewSet):
|
||||||
|
serializer = PlaylistSerializer
|
||||||
|
model = controllers.PlaylistSource
|
||||||
|
|
||||||
|
|
||||||
|
class QueueSourceViewSet(SourceViewSet):
|
||||||
|
serializer = QueueSourceSerializer
|
||||||
|
model = controllers.QueueSource
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def push(self, request, pk):
|
||||||
|
self.get_source(pk).push()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import Vue from 'vue';
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||||
import 'buefy/dist/buefy.css';
|
|
||||||
|
|
||||||
|
|
||||||
//-- aircox
|
//-- aircox
|
||||||
|
|
|
@ -78,7 +78,7 @@ a.navbar-item.is-active {
|
||||||
.card-super-title {
|
.card-super-title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: 1.2em;
|
font-size: $size-6;
|
||||||
font-weight: $weight-bold;
|
font-weight: $weight-bold;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
|
|
|
@ -15,8 +15,8 @@ Including another URLconf
|
||||||
"""
|
"""
|
||||||
# from django.conf.urls.i18n import i18n_patterns
|
# from django.conf.urls.i18n import i18n_patterns
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import include, path, re_path
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path, re_path
|
||||||
|
|
||||||
import aircox.urls
|
import aircox.urls
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user