This commit is contained in:
bkfox 2019-09-23 12:38:44 +02:00
parent cbfd1c1449
commit b3ec6f670f
29 changed files with 790 additions and 85 deletions

7
aircox/admin/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))

View File

@ -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'),

View File

@ -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& ***!

View File

@ -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& ***!

View 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?");
/***/ })
/******/ });

View File

@ -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>

View File

@ -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 %}

View 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()

View File

@ -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'),
]

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
View 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

View File

@ -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 #}

View File

@ -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'):

View File

@ -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
View 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)
}

View File

@ -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; }