update
This commit is contained in:
parent
cbfd1c1449
commit
b3ec6f670f
7
aircox/admin/__init__.py
Normal file
7
aircox/admin/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
|
18
aircox/admin/article.py
Normal file
18
aircox/admin/article.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
|
||||
|
58
aircox/admin/episode.py
Normal file
58
aircox/admin/episode.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
import copy
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from ..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]
|
||||
|
||||
|
13
aircox/admin/log.py
Normal file
13
aircox/admin/log.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
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']
|
||||
|
42
aircox/admin/mixins.py
Normal file
42
aircox/admin/mixins.py
Normal file
|
@ -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
|
||||
|
||||
|
52
aircox/admin/page.py
Normal file
52
aircox/admin/page.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
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
|
||||
|
||||
|
||||
|
76
aircox/admin/program.py
Normal file
76
aircox/admin/program.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
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')
|
||||
|
||||
|
||||
|
65
aircox/admin/sound.py
Normal file
65
aircox/admin/sound.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
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']
|
||||
|
||||
|
15
aircox/admin/station.py
Normal file
15
aircox/admin/station.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from ..models import Station
|
||||
from .page import NavItemInline
|
||||
|
||||
|
||||
__all__ = ['StationAdmin']
|
||||
|
||||
|
||||
@admin.register(Station)
|
||||
class StationAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
inlines = (NavItemInline,)
|
||||
|
||||
|
56
aircox/admin_site.py
Normal file
56
aircox/admin_site.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include, reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .models import Program
|
||||
from .views.admin import StatisticsView
|
||||
|
||||
|
||||
__all__ = ['AdminSite']
|
||||
|
||||
|
||||
class AdminSite(admin.AdminSite):
|
||||
extra_urls = None
|
||||
tools = [
|
||||
(_('Statistics'), 'admin:tools-stats'),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.router = DefaultRouter()
|
||||
self.extra_urls = []
|
||||
self.tools = type(self).tools.copy()
|
||||
|
||||
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):
|
||||
urls = super().get_urls() + [
|
||||
path('api/', include((self.router.urls, 'api'))),
|
||||
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'),
|
||||
] + self.extra_urls
|
||||
return urls
|
||||
|
||||
def get_tools(self):
|
||||
return [(label, reverse(url)) for label, url in self.tools]
|
||||
|
||||
def route_view(self, url, view, name, admin_view=True, label=None):
|
||||
self.extra_urls.append(path(
|
||||
url, self.admin_view(view) if admin_view else view, name=name
|
||||
))
|
||||
|
||||
if label:
|
||||
self.tools.append((label, 'admin:' + name))
|
||||
|
||||
|
Binary file not shown.
|
@ -34,6 +34,9 @@ class SoundQuerySet(models.QuerySet):
|
|||
id = diffusion.pk if id is None else id
|
||||
return self.filter(episode__diffusion__id=id)
|
||||
|
||||
def available(self):
|
||||
return self.exclude(type=Sound.TYPE_REMOVED)
|
||||
|
||||
def podcasts(self):
|
||||
""" Return sounds available as podcasts """
|
||||
return self.filter(Q(embed__isnull=False) | Q(is_public=True))
|
||||
|
@ -87,6 +90,9 @@ class Sound(models.Model):
|
|||
verbose_name=_('episode'),
|
||||
)
|
||||
type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_('order'), default=0, help_text=_('position in the playlist'),
|
||||
)
|
||||
# FIXME: url() does not use the same directory than here
|
||||
# should we use FileField for more reliability?
|
||||
path = models.FilePathField(
|
||||
|
@ -278,9 +284,7 @@ class Track(models.Model):
|
|||
verbose_name=_('sound'),
|
||||
)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_('order'),
|
||||
default=0,
|
||||
help_text=_('position in the playlist'),
|
||||
_('order'), default=0, help_text=_('position in the playlist'),
|
||||
)
|
||||
timestamp = models.PositiveSmallIntegerField(
|
||||
_('timestamp'),
|
||||
|
|
|
@ -282,7 +282,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue_
|
|||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (class {\n constructor(url, timeout) {\n this.url = url;\n this.timeout = timeout;\n this.promise = null;\n this.items = [];\n }\n\n drop() {\n this.promise = null;\n }\n\n fetch() {\n const promise = fetch(this.url).then(response =>\n response.ok ? response.json()\n : Promise.reject(response)\n ).then(data => {\n this.items = data;\n return this.items\n })\n\n this.promise = promise;\n return promise;\n }\n\n refresh() {\n const promise = this.fetch();\n promise.then(data => {\n if(promise != this.promise)\n return [];\n\n window.setTimeout(() => this.refresh(), this.timeout*1000)\n })\n return promise\n }\n});\n\n\n//# sourceURL=webpack:///./assets/public/liveInfo.js?");
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var public_utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! public/utils */ \"./assets/public/utils.js\");\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (class {\n constructor(url, timeout) {\n this.url = url;\n this.timeout = timeout;\n this.promise = null;\n this.items = [];\n }\n\n drop() {\n this.promise = null;\n }\n\n fetch() {\n const promise = fetch(this.url).then(response =>\n response.ok ? response.json()\n : Promise.reject(response)\n ).then(data => {\n this.items = data;\n return this.items\n })\n\n this.promise = promise;\n return promise;\n }\n\n refresh() {\n const promise = this.fetch();\n promise.then(data => {\n if(promise != this.promise)\n return [];\n\n Object(public_utils__WEBPACK_IMPORTED_MODULE_0__[\"setEcoTimeout\"])(() => this.refresh(), this.timeout*1000)\n })\n return promise\n }\n});\n\n\n//# sourceURL=webpack:///./assets/public/liveInfo.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
|
@ -333,6 +333,18 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse
|
|||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/public/utils.js":
|
||||
/*!********************************!*\
|
||||
!*** ./assets/public/utils.js ***!
|
||||
\********************************/
|
||||
/*! exports provided: setEcoTimeout, setEcoInterval */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoTimeout\", function() { return setEcoTimeout; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoInterval\", function() { return setEcoInterval; });\n\n\nfunction setEcoTimeout(func, ...args) {\n return setTimeout((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\nfunction setEcoInterval(func, ...args) {\n return setInterval((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/utils.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/admin/statistics.vue?vue&type=script&lang=js&":
|
||||
/*!****************************************************************************************************************!*\
|
||||
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/admin/statistics.vue?vue&type=script&lang=js& ***!
|
||||
|
|
|
@ -223,7 +223,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue_
|
|||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (class {\n constructor(url, timeout) {\n this.url = url;\n this.timeout = timeout;\n this.promise = null;\n this.items = [];\n }\n\n drop() {\n this.promise = null;\n }\n\n fetch() {\n const promise = fetch(this.url).then(response =>\n response.ok ? response.json()\n : Promise.reject(response)\n ).then(data => {\n this.items = data;\n return this.items\n })\n\n this.promise = promise;\n return promise;\n }\n\n refresh() {\n const promise = this.fetch();\n promise.then(data => {\n if(promise != this.promise)\n return [];\n\n window.setTimeout(() => this.refresh(), this.timeout*1000)\n })\n return promise\n }\n});\n\n\n//# sourceURL=webpack:///./assets/public/liveInfo.js?");
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var public_utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! public/utils */ \"./assets/public/utils.js\");\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (class {\n constructor(url, timeout) {\n this.url = url;\n this.timeout = timeout;\n this.promise = null;\n this.items = [];\n }\n\n drop() {\n this.promise = null;\n }\n\n fetch() {\n const promise = fetch(this.url).then(response =>\n response.ok ? response.json()\n : Promise.reject(response)\n ).then(data => {\n this.items = data;\n return this.items\n })\n\n this.promise = promise;\n return promise;\n }\n\n refresh() {\n const promise = this.fetch();\n promise.then(data => {\n if(promise != this.promise)\n return [];\n\n Object(public_utils__WEBPACK_IMPORTED_MODULE_0__[\"setEcoTimeout\"])(() => this.refresh(), this.timeout*1000)\n })\n return promise\n }\n});\n\n\n//# sourceURL=webpack:///./assets/public/liveInfo.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
|
@ -274,6 +274,18 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse
|
|||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/public/utils.js":
|
||||
/*!********************************!*\
|
||||
!*** ./assets/public/utils.js ***!
|
||||
\********************************/
|
||||
/*! exports provided: setEcoTimeout, setEcoInterval */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoTimeout\", function() { return setEcoTimeout; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoInterval\", function() { return setEcoInterval; });\n\n\nfunction setEcoTimeout(func, ...args) {\n return setTimeout((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\nfunction setEcoInterval(func, ...args) {\n return setInterval((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/utils.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=script&lang=js&":
|
||||
/*!*******************************************************************************************************************!*\
|
||||
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/public/autocomplete.vue?vue&type=script&lang=js& ***!
|
||||
|
|
230
aircox/static/aircox/streamer.js
Normal file
230
aircox/static/aircox/streamer.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // install a JSONP callback for chunk loading
|
||||
/******/ function webpackJsonpCallback(data) {
|
||||
/******/ var chunkIds = data[0];
|
||||
/******/ var moreModules = data[1];
|
||||
/******/ var executeModules = data[2];
|
||||
/******/
|
||||
/******/ // add "moreModules" to the modules object,
|
||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
||||
/******/ var moduleId, chunkId, i = 0, resolves = [];
|
||||
/******/ for(;i < chunkIds.length; i++) {
|
||||
/******/ chunkId = chunkIds[i];
|
||||
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
|
||||
/******/ resolves.push(installedChunks[chunkId][0]);
|
||||
/******/ }
|
||||
/******/ installedChunks[chunkId] = 0;
|
||||
/******/ }
|
||||
/******/ for(moduleId in moreModules) {
|
||||
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
|
||||
/******/ modules[moduleId] = moreModules[moduleId];
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
|
||||
/******/
|
||||
/******/ while(resolves.length) {
|
||||
/******/ resolves.shift()();
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // add entry modules from loaded chunk to deferred list
|
||||
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
|
||||
/******/
|
||||
/******/ // run deferred modules when all chunks ready
|
||||
/******/ return checkDeferredModules();
|
||||
/******/ };
|
||||
/******/ function checkDeferredModules() {
|
||||
/******/ var result;
|
||||
/******/ for(var i = 0; i < deferredModules.length; i++) {
|
||||
/******/ var deferredModule = deferredModules[i];
|
||||
/******/ var fulfilled = true;
|
||||
/******/ for(var j = 1; j < deferredModule.length; j++) {
|
||||
/******/ var depId = deferredModule[j];
|
||||
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
|
||||
/******/ }
|
||||
/******/ if(fulfilled) {
|
||||
/******/ deferredModules.splice(i--, 1);
|
||||
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ return result;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // object to store loaded and loading chunks
|
||||
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
||||
/******/ // Promise = chunk loading, 0 = chunk loaded
|
||||
/******/ var installedChunks = {
|
||||
/******/ "streamer": 0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ var deferredModules = [];
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
|
||||
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
|
||||
/******/ jsonpArray.push = webpackJsonpCallback;
|
||||
/******/ jsonpArray = jsonpArray.slice();
|
||||
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
|
||||
/******/ var parentJsonpFunction = oldJsonpFunction;
|
||||
/******/
|
||||
/******/
|
||||
/******/ // add entry module to deferred list
|
||||
/******/ deferredModules.push(["./assets/streamer/index.js","vendor"]);
|
||||
/******/ // run deferred modules when ready
|
||||
/******/ return checkDeferredModules();
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ "./assets/public/app.js":
|
||||
/*!******************************!*\
|
||||
!*** ./assets/public/app.js ***!
|
||||
\******************************/
|
||||
/*! exports provided: appBaseConfig, setAppConfig, getAppConfig, loadApp */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"appBaseConfig\", function() { return appBaseConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setAppConfig\", function() { return setAppConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getAppConfig\", function() { return getAppConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"loadApp\", function() { return loadApp; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\n\nconst appBaseConfig = {\n el: '#app',\n delimiters: ['[[', ']]'],\n}\n\n/**\n * Application config for the main application instance\n */\nvar appConfig = {};\n\nfunction setAppConfig(config) {\n for(var member in appConfig) delete appConfig[member];\n return Object.assign(appConfig, config)\n}\n\nfunction getAppConfig(config) {\n if(config instanceof Function)\n config = config()\n config = config == null ? appConfig : config;\n return {...appBaseConfig, ...config}\n}\n\n\n/**\n * Create Vue application at window 'load' event and return a Promise\n * resolving to the created app.\n *\n * config: defaults to appConfig (checked when window is loaded)\n */\nfunction loadApp(config=null) {\n return new Promise(function(resolve, reject) {\n window.addEventListener('load', function() {\n try {\n config = getAppConfig(config)\n const el = document.querySelector(config.el)\n if(!el) {\n reject(`Error: missing element ${config.el}`);\n return;\n }\n\n resolve(new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"](config))\n }\n catch(error) { reject(error) }\n })\n })\n}\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/public/model.js":
|
||||
/*!********************************!*\
|
||||
!*** ./assets/public/model.js ***!
|
||||
\********************************/
|
||||
/*! exports provided: getCsrf, default */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getCsrf\", function() { return getCsrf; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Model; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\nfunction getCookie(name) {\n if(document.cookie && document.cookie !== '') {\n const cookie = document.cookie.split(';')\n .find(c => c.trim().startsWith(name + '='))\n return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;\n }\n return null;\n}\n\nvar csrfToken = null;\n\nfunction getCsrf() {\n if(csrfToken === null)\n csrfToken = getCookie('csrftoken')\n return csrfToken;\n}\n\n\n// TODO: move in another module for reuse\n// TODO: prevent duplicate simple fetch\nclass Model {\n constructor(data, {url=null}={}) {\n this.commit(data);\n }\n\n static getId(data) {\n return data.id;\n }\n\n static getOptions(options) {\n return {\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'X-CSRFToken': getCsrf(),\n },\n ...options,\n }\n }\n\n static fetch(url, options=null, initArgs=null) {\n options = this.getOptions(options)\n return fetch(url, options)\n .then(response => response.json())\n .then(data => new this(d, {url: url, ...initArgs}));\n }\n\n static fetchAll(url, options=null, initArgs=null) {\n options = this.getOptions(options)\n return fetch(url, options)\n .then(response => response.json())\n .then(data => {\n if(!(data instanceof Array))\n data = data.results;\n data = data.map(d => new this(d, {baseUrl: url, ...initArgs}));\n return data\n })\n }\n\n /**\n * Fetch data from server.\n */\n fetch(options) {\n options = this.constructor.getOptions(options)\n return fetch(this.url, options)\n .then(response => response.json())\n .then(data => this.commit(data));\n }\n\n /**\n * Call API action on object.\n */\n action(path, options, commit=false) {\n options = this.constructor.getOptions(options)\n const promise = fetch(this.url + path, options);\n return commit ? promise.then(data => data.json())\n .then(data => { this.commit(data); this.data })\n : promise;\n }\n\n /**\n * Update instance's data with provided data. Return None\n */\n commit(data) {\n this.id = this.constructor.getId(data);\n this.url = data.url_;\n vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].set(this, 'data', data);\n }\n\n /**\n * Return data as model with url prepent by `this.url`.\n */\n asChild(model, data, prefix='') {\n return new model(data, {baseUrl: `${this.url}${prefix}/`})\n }\n\n getChildOf(attr, id) {\n const index = this.data[attr].findIndex(o => o.id = id)\n return index == -1 ? null : this.data[attr][index];\n }\n\n static updateList(list=[], old=[], ...initArgs) {\n return list.reduce((items, data) => {\n const id = this.getId(data);\n let [index, obj] = [old.findIndex(o => o.id == id), null];\n if(index != -1) {\n old[index].commit(data)\n items.push(old[index]);\n }\n else\n items.push(new this(data, ...initArgs))\n return items;\n }, [])\n }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/model.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/public/sound.js":
|
||||
/*!********************************!*\
|
||||
!*** ./assets/public/sound.js ***!
|
||||
\********************************/
|
||||
/*! exports provided: default */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Sound; });\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./model */ \"./assets/public/model.js\");\n\n\n\nclass Sound extends _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"] {\n get name() { return this.data.name }\n\n static getId(data) { return data.pk }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/sound.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/public/utils.js":
|
||||
/*!********************************!*\
|
||||
!*** ./assets/public/utils.js ***!
|
||||
\********************************/
|
||||
/*! exports provided: setEcoTimeout, setEcoInterval */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoTimeout\", function() { return setEcoTimeout; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setEcoInterval\", function() { return setEcoInterval; });\n\n\nfunction setEcoTimeout(func, ...args) {\n return setTimeout((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\nfunction setEcoInterval(func, ...args) {\n return setInterval((...args) => {\n !document.hidden && func(...args)\n }, ...args)\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/utils.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/streamer/controllers.js":
|
||||
/*!****************************************!*\
|
||||
!*** ./assets/streamer/controllers.js ***!
|
||||
\****************************************/
|
||||
/*! exports provided: Streamer, Request, Source, Playlist, Queue */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"Streamer\", function() { return Streamer; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"Request\", function() { return Request; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"Source\", function() { return Source; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"Playlist\", function() { return Playlist; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"Queue\", function() { return Queue; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var public_model__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! public/model */ \"./assets/public/model.js\");\n/* harmony import */ var public_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! public/utils */ \"./assets/public/utils.js\");\n\n\n\n\n\n\nclass Streamer extends public_model__WEBPACK_IMPORTED_MODULE_1__[\"default\"] {\n get queues() { return this.data ? this.data.queues : []; }\n get playlists() { return this.data ? this.data.playlists : []; }\n get sources() { return [...this.queues, ...this.playlists]; }\n get source() { return this.sources.find(o => o.id == this.data.source) }\n\n commit(data) {\n if(!this.data)\n this.data = { id: data.id, playlists: [], queues: [] }\n\n data.playlists = Playlist.updateList(data.playlists, this.playlists, {streamer: this})\n data.queues = Queue.updateList(data.queues, this.queues, {streamer: this})\n super.commit(data)\n }\n}\n\nclass Request extends public_model__WEBPACK_IMPORTED_MODULE_1__[\"default\"] {\n static getId(data) { return data.rid; }\n}\n\nclass Source extends public_model__WEBPACK_IMPORTED_MODULE_1__[\"default\"] {\n constructor(data, {streamer=null, ...options}={}) {\n super(data, options);\n this.streamer = streamer;\n Object(public_utils__WEBPACK_IMPORTED_MODULE_2__[\"setEcoInterval\"])(() => this.tick(), 1000)\n }\n\n get isQueue() { return false; }\n get isPlaylist() { return false; }\n get isPlaying() { return this.data.status == 'playing' }\n get isPaused() { return this.data.status == 'paused' }\n\n get remainingString() {\n if(!this.remaining)\n return '00:00';\n\n const seconds = Math.floor(this.remaining % 60);\n const minutes = Math.floor(this.remaining / 60);\n return String(minutes).padStart(2, '0') + ':' +\n String(seconds).padStart(2, '0');\n }\n\n sync() { return this.action('sync/', {method: 'POST'}, true); }\n skip() { return this.action('skip/', {method: 'POST'}, true); }\n restart() { return this.action('restart/', {method: 'POST'}, true); }\n\n seek(count) {\n return this.action('seek/', {\n method: 'POST',\n body: JSON.stringify({count: count})\n }, true)\n }\n\n tick() {\n if(!this.data.remaining || !this.isPlaying)\n return;\n const delta = (Date.now() - this.commitDate) / 1000;\n vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].set(this, 'remaining', this.data.remaining - delta)\n }\n\n commit(data) {\n if(data.air_time)\n data.air_time = new Date(data.air_time);\n\n this.commitDate = Date.now()\n super.commit(data)\n vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].set(this, 'remaining', data.remaining)\n }\n}\n\n\nclass Playlist extends Source {\n get isPlaylist() { return true; }\n}\n\n\nclass Queue extends Source {\n get isQueue() { return true; }\n get queue() { return this.data && this.data.queue; }\n\n commit(data) {\n data.queue = Request.updateList(data.queue, this.queue)\n super.commit(data)\n }\n\n push(soundId) {\n return this.action('push/', {\n method: 'POST',\n body: JSON.stringify({'sound_id': parseInt(soundId)})\n }, true);\n }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/streamer/controllers.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./assets/streamer/index.js":
|
||||
/*!**********************************!*\
|
||||
!*** ./assets/streamer/index.js ***!
|
||||
\**********************************/
|
||||
/*! no exports provided */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"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 buefy_dist_components_button__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy/dist/components/button */ \"./node_modules/buefy/dist/components/button/index.js\");\n/* harmony import */ var buefy_dist_components_button__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy_dist_components_button__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var public_app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! public/app */ \"./assets/public/app.js\");\n/* harmony import */ var public_model__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! public/model */ \"./assets/public/model.js\");\n/* harmony import */ var public_sound__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! public/sound */ \"./assets/public/sound.js\");\n/* harmony import */ var _controllers__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./controllers */ \"./assets/streamer/controllers.js\");\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(buefy_dist_components_button__WEBPACK_IMPORTED_MODULE_1___default.a)\n\n\n\n\n\n\nwindow.aircox.appConfig = {\n data() {\n return {\n // current streamer\n streamer: null,\n // all streamers\n streamers: [],\n // fetch interval id\n fetchInterval: null,\n\n Sound: public_sound__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n }\n },\n\n computed: {\n apiUrl() {\n return this.$el && this.$el.dataset.apiUrl;\n },\n\n sources() {\n var sources = this.streamer ? this.streamer.sources : [];\n return sources.filter(s => s.data)\n },\n },\n\n methods: {\n fetchStreamers() {\n _controllers__WEBPACK_IMPORTED_MODULE_5__[\"Streamer\"].fetchAll(this.apiUrl, null)\n .then(streamers => {\n vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].set(this, 'streamers', streamers);\n vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].set(this, 'streamer', streamers ? streamers[0] : null);\n })\n },\n },\n\n mounted() {\n this.fetchStreamers();\n this.fetchInterval = setInterval(() => this.streamer && this.streamer.fetch(), 5000)\n },\n\n destroyed() {\n if(this.fetchInterval !== null)\n clearInterval(this.fetchInterval)\n }\n}\n\n\n\n//# sourceURL=webpack:///./assets/streamer/index.js?");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
|
@ -133,7 +133,7 @@ Blocks:
|
|||
|
||||
<hr>
|
||||
</div>
|
||||
<div id="player">{% include "aircox/player.html" %}</div>
|
||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</noscript>
|
||||
|
||||
<a-player ref="player" src="{{ audio_streams.0 }}"
|
||||
live-info-url="{% url "api-live" %}" :live-info-timeout="20"
|
||||
live-info-url="{% url "api:live" %}" :live-info-timeout="20"
|
||||
button-title="{% trans "Play or pause audio" %}">
|
||||
<template v-slot:sources>
|
||||
{% for stream in audio_streams %}
|
9
aircox/templatetags/aircox_admin.py
Normal file
9
aircox/templatetags/aircox_admin.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django import template
|
||||
from django.contrib import admin
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag(name='get_admin_tools')
|
||||
def do_get_admin_tools():
|
||||
return admin.site.get_tools()
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
from django.urls import include, path, register_converter
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import views, models
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from . import models, views, viewsets
|
||||
from .converters import PagePathConverter, DateConverter, WeekConverter
|
||||
|
||||
|
||||
__all__ = ['api', 'urls']
|
||||
|
||||
|
||||
register_converter(PagePathConverter, 'page_path')
|
||||
register_converter(DateConverter, 'date')
|
||||
register_converter(WeekConverter, 'week')
|
||||
|
@ -17,14 +22,18 @@ register_converter(WeekConverter, 'week')
|
|||
# ]
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('sound', viewsets.SoundViewSet, basename='sound')
|
||||
|
||||
|
||||
api = [
|
||||
path('logs/', views.api.LogListAPIView.as_view(), name='api-live'),
|
||||
]
|
||||
path('logs/', views.LogListAPIView.as_view(), name='live'),
|
||||
] + router.urls
|
||||
|
||||
|
||||
urls = [
|
||||
path('', views.HomeView.as_view(), name='home'),
|
||||
path('api/', include(api)),
|
||||
path('api/', include((api, 'aircox'), namespace='api')),
|
||||
|
||||
# path('', views.PageDetailView.as_view(model=models.Article),
|
||||
# name='home'),
|
||||
|
@ -61,6 +70,5 @@ urls = [
|
|||
views.ArticleListView.as_view(), name='article-list'),
|
||||
path(_('programs/<slug:parent_slug>/publications/'),
|
||||
views.ProgramPageListView.as_view(), name='program-page-list'),
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from . import api, admin
|
||||
from . import admin
|
||||
|
||||
from .base import BaseView
|
||||
from .base import BaseView, BaseAPIView
|
||||
from .home import HomeView
|
||||
|
||||
from .article import ArticleDetailView, ArticleListView
|
||||
from .episode import EpisodeDetailView, EpisodeListView, DiffusionListView
|
||||
from .log import LogListView
|
||||
from .log import LogListView, LogListAPIView
|
||||
from .page import PageDetailView, PageListView
|
||||
from .program import ProgramDetailView, ProgramListView, \
|
||||
ProgramPageDetailView, ProgramPageListView
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import datetime
|
||||
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from ..models import Log
|
||||
from ..serializers import LogInfo, LogInfoSerializer
|
||||
from .log import LogListMixin
|
||||
|
||||
|
||||
class BaseAPIView:
|
||||
@property
|
||||
def station(self):
|
||||
return self.request.station
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().station(self.station)
|
||||
|
||||
|
||||
class LogListAPIView(LogListMixin, BaseAPIView, ListAPIView):
|
||||
"""
|
||||
Return logs list, including diffusions. By default return logs of
|
||||
the last 30 minutes.
|
||||
|
||||
Available GET parameters:
|
||||
- "date": return logs for a specified date (
|
||||
- "full": (staff user only) don't merge diffusion and logs
|
||||
"""
|
||||
serializer_class = LogInfoSerializer
|
||||
queryset = Log.objects.all()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.date = self.get_date()
|
||||
if self.date is None:
|
||||
self.min_date = tz.now() - tz.timedelta(minutes=30)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_object_list(self, logs, full):
|
||||
return [LogInfo(obj) for obj in super().get_object_list(logs, full)]
|
||||
|
||||
def get_serializer(self, queryset, *args, **kwargs):
|
||||
full = bool(self.request.GET.get('full'))
|
||||
return super().get_serializer(self.get_object_list(queryset, full),
|
||||
*args, **kwargs)
|
||||
|
|
@ -59,3 +59,12 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BaseAPIView:
|
||||
@property
|
||||
def station(self):
|
||||
return self.request.station
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().station(self.station)
|
||||
|
||||
|
||||
|
|
|
@ -4,8 +4,13 @@ import datetime
|
|||
from django.views.generic import ListView
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from ..models import Diffusion, Log
|
||||
from .base import BaseView
|
||||
from ..serializers import LogInfo, LogInfoSerializer
|
||||
from .base import BaseView, BaseAPIView
|
||||
from .mixins import GetDateMixin
|
||||
|
||||
|
||||
|
@ -68,3 +73,30 @@ class LogListView(BaseView, LogListMixin, ListView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
# Logs are accessible through API only with this list view
|
||||
class LogListAPIView(LogListMixin, BaseAPIView, ListAPIView):
|
||||
"""
|
||||
Return logs list, including diffusions. By default return logs of
|
||||
the last 30 minutes.
|
||||
|
||||
Available GET parameters:
|
||||
- "date": return logs for a specified date (
|
||||
- "full": (staff user only) don't merge diffusion and logs
|
||||
"""
|
||||
serializer_class = LogInfoSerializer
|
||||
queryset = Log.objects.all()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.date = self.get_date()
|
||||
if self.date is None:
|
||||
self.min_date = tz.now() - tz.timedelta(minutes=30)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_object_list(self, logs, full):
|
||||
return [LogInfo(obj) for obj in super().get_object_list(logs, full)]
|
||||
|
||||
def get_serializer(self, queryset, *args, **kwargs):
|
||||
full = bool(self.request.GET.get('full'))
|
||||
return super().get_serializer(self.get_object_list(queryset, full),
|
||||
*args, **kwargs)
|
||||
|
||||
|
|
26
aircox/viewsets.py
Normal file
26
aircox/viewsets.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from rest_framework import viewsets
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from .models import Sound
|
||||
from .serializers import SoundSerializer
|
||||
from .views import BaseAPIView
|
||||
|
||||
|
||||
class SoundFilter(filters.FilterSet):
|
||||
station = filters.NumberFilter(field_name='program__station__id')
|
||||
program = filters.NumberFilter(field_name='program_id')
|
||||
episode = filters.NumberFilter(field_name='episode_id')
|
||||
search = filters.CharFilter(field_name='search', method='search_filter')
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.search(value)
|
||||
|
||||
|
||||
class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
||||
serializer_class = SoundSerializer
|
||||
queryset = Sound.objects.available().order_by('-pk')
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = SoundFilter
|
||||
|
|
@ -41,7 +41,8 @@
|
|||
<h6 class="title is-6 is-marginless">{% trans "Add sound" %}</h6>
|
||||
<form class="columns" @submit.prevent="source.push($event.target.elements['sound_id'].value)">
|
||||
<div class="column field is-small">
|
||||
<a-autocomplete url="{% url "admin:api:streamer-queue-autocomplete-push" station_pk=station.pk %}?q=${query}"
|
||||
{# TODO: select station => change the shit #}
|
||||
<a-autocomplete url="{% url "aircox:sound-list" %}?station={{ station.pk }}&search=${query}"
|
||||
class="is-fullwidth"
|
||||
:model="Sound" field="name" value-field="sound_id" value-attr="id"
|
||||
{# FIXME dirty hack awaiting the vue component #}
|
||||
|
|
|
@ -73,7 +73,7 @@ streamers = Streamers()
|
|||
|
||||
class BaseControllerAPIView(viewsets.ViewSet):
|
||||
permission_classes = (IsAdminUser,)
|
||||
serializer = None
|
||||
serializer_class = None
|
||||
streamer = None
|
||||
object = None
|
||||
|
||||
|
@ -85,7 +85,7 @@ class BaseControllerAPIView(viewsets.ViewSet):
|
|||
return streamers[id]
|
||||
|
||||
def get_serializer(self, **kwargs):
|
||||
return self.serializer(self.object, **kwargs)
|
||||
return self.serializer_class(self.object, **kwargs)
|
||||
|
||||
def serialize(self, obj, **kwargs):
|
||||
self.object = obj
|
||||
|
@ -98,11 +98,11 @@ class BaseControllerAPIView(viewsets.ViewSet):
|
|||
|
||||
|
||||
class RequestViewSet(BaseControllerAPIView):
|
||||
serializer = RequestSerializer
|
||||
serializer_class = RequestSerializer
|
||||
|
||||
|
||||
class StreamerViewSet(BaseControllerAPIView):
|
||||
serializer = StreamerSerializer
|
||||
serializer_class = StreamerSerializer
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
return Response(self.serialize(self.streamer))
|
||||
|
@ -121,7 +121,7 @@ class StreamerViewSet(BaseControllerAPIView):
|
|||
|
||||
|
||||
class SourceViewSet(BaseControllerAPIView):
|
||||
serializer = SourceSerializer
|
||||
serializer_class = SourceSerializer
|
||||
model = controllers.Source
|
||||
|
||||
def get_sources(self):
|
||||
|
@ -168,27 +168,17 @@ class SourceViewSet(BaseControllerAPIView):
|
|||
|
||||
|
||||
class PlaylistSourceViewSet(SourceViewSet):
|
||||
serializer = PlaylistSerializer
|
||||
serializer_class = PlaylistSerializer
|
||||
model = controllers.PlaylistSource
|
||||
|
||||
|
||||
class QueueSourceViewSet(SourceViewSet):
|
||||
serializer = QueueSourceSerializer
|
||||
serializer_class = QueueSourceSerializer
|
||||
model = controllers.QueueSource
|
||||
|
||||
def get_sound_queryset(self):
|
||||
return Sound.objects.station(self.request.station).archive()
|
||||
|
||||
@action(detail=False, url_path='autocomplete/push',
|
||||
url_name='autocomplete-push')
|
||||
def autcomplete_push(self, request):
|
||||
query = request.GET.get('q')
|
||||
qs = self.get_sound_queryset().search(query)
|
||||
serializer = SoundSerializer(qs, many=True, context={
|
||||
'request': self.request
|
||||
})
|
||||
return Response({'results': serializer.data})
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def push(self, request, pk):
|
||||
if not request.data.get('sound_id'):
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {setEcoTimeout} from 'public/utils';
|
||||
|
||||
|
||||
export default class {
|
||||
constructor(url, timeout) {
|
||||
|
@ -30,7 +32,7 @@ export default class {
|
|||
if(promise != this.promise)
|
||||
return [];
|
||||
|
||||
window.setTimeout(() => this.refresh(), this.timeout*1000)
|
||||
setEcoTimeout(() => this.refresh(), this.timeout*1000)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
|
15
assets/public/utils.js
Normal file
15
assets/public/utils.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
|
||||
export function setEcoTimeout(func, ...args) {
|
||||
return setTimeout((...args) => {
|
||||
!document.hidden && func(...args)
|
||||
}, ...args)
|
||||
}
|
||||
|
||||
|
||||
export function setEcoInterval(func, ...args) {
|
||||
return setInterval((...args) => {
|
||||
!document.hidden && func(...args)
|
||||
}, ...args)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import Model from 'public/model';
|
||||
import {setEcoInterval} from 'public/utils';
|
||||
|
||||
|
||||
export class Streamer extends Model {
|
||||
|
@ -27,7 +28,7 @@ export class Source extends Model {
|
|||
constructor(data, {streamer=null, ...options}={}) {
|
||||
super(data, options);
|
||||
this.streamer = streamer;
|
||||
setInterval(() => this.tick(), 1000)
|
||||
setEcoInterval(() => this.tick(), 1000)
|
||||
}
|
||||
|
||||
get isQueue() { return false; }
|
||||
|
|
Loading…
Reference in New Issue
Block a user