streamer as separate application; working streamer monitor interface
This commit is contained in:
parent
4e61ec1520
commit
d3f39c5ade
|
@ -108,7 +108,7 @@ class PlaylistImport:
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
||||||
class Command (BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = __doc__
|
help = __doc__
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
|
Binary file not shown.
|
@ -22,13 +22,17 @@ __all__ = ['Sound', 'SoundQuerySet', 'Track']
|
||||||
|
|
||||||
|
|
||||||
class SoundQuerySet(models.QuerySet):
|
class SoundQuerySet(models.QuerySet):
|
||||||
|
def station(self, station=None, id=None):
|
||||||
|
id = station.pk if id is None else id
|
||||||
|
return self.filter(program__station__id=id)
|
||||||
|
|
||||||
def episode(self, episode=None, id=None):
|
def episode(self, episode=None, id=None):
|
||||||
return self.filter(episode=episode) if id is None else \
|
id = episode.pk if id is None else id
|
||||||
self.filter(episode__id=id)
|
return self.filter(episode__id=id)
|
||||||
|
|
||||||
def diffusion(self, diffusion=None, id=None):
|
def diffusion(self, diffusion=None, id=None):
|
||||||
return self.filter(episode__diffusion=diffusion) if id is None else \
|
id = diffusion.pk if id is None else id
|
||||||
self.filter(episode__diffusion__id=id)
|
return self.filter(episode__diffusion__id=id)
|
||||||
|
|
||||||
def podcasts(self):
|
def podcasts(self):
|
||||||
""" Return sounds available as podcasts """
|
""" Return sounds available as podcasts """
|
||||||
|
@ -49,6 +53,13 @@ class SoundQuerySet(models.QuerySet):
|
||||||
self = self.order_by('path')
|
self = self.order_by('path')
|
||||||
return self.filter(path__isnull=False).values_list('path', flat=True)
|
return self.filter(path__isnull=False).values_list('path', flat=True)
|
||||||
|
|
||||||
|
def search(self, query):
|
||||||
|
return self.filter(
|
||||||
|
Q(name__icontains=query) | Q(path__icontains=query) |
|
||||||
|
Q(program__title__icontains=query) |
|
||||||
|
Q(episode__title__icontains=query)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Sound(models.Model):
|
class Sound(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Diffusion, Log
|
from .models import Diffusion, Log, Sound
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LogInfo', 'LogInfoSerializer']
|
__all__ = ['LogInfo', 'LogInfoSerializer']
|
||||||
|
@ -53,3 +53,18 @@ class LogInfoSerializer(serializers.Serializer):
|
||||||
cover = serializers.URLField(required=False)
|
cover = serializers.URLField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundSerializer(serializers.ModelSerializer):
|
||||||
|
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Sound
|
||||||
|
fields = ['pk', 'name', 'path', 'program', 'episode', 'embed', 'type',
|
||||||
|
'duration', 'mtime', 'is_good_quality', 'is_public']
|
||||||
|
|
||||||
|
def get_field_names(self, *args):
|
||||||
|
names = super().get_field_names(*args)
|
||||||
|
if not self.context['request'].user.is_staff and self.instance \
|
||||||
|
and not self.instance.is_public:
|
||||||
|
names.remove('path')
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
|
@ -207,6 +207,97 @@ ul.menu-list li {
|
||||||
fieldset[disabled] .pagination-ellipsis {
|
fieldset[disabled] .pagination-ellipsis {
|
||||||
cursor: not-allowed; }
|
cursor: not-allowed; }
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top; }
|
||||||
|
.dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu {
|
||||||
|
display: block; }
|
||||||
|
.dropdown.is-right .dropdown-menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0; }
|
||||||
|
.dropdown.is-up .dropdown-menu {
|
||||||
|
bottom: 100%;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
padding-top: initial;
|
||||||
|
top: auto; }
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
min-width: 12rem;
|
||||||
|
padding-top: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 20; }
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.5rem; }
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: #4a4a4a;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
a.dropdown-item,
|
||||||
|
button.dropdown-item {
|
||||||
|
padding-right: 3rem;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%; }
|
||||||
|
a.dropdown-item:hover,
|
||||||
|
button.dropdown-item:hover {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
color: #0a0a0a; }
|
||||||
|
a.dropdown-item.is-active,
|
||||||
|
button.dropdown-item.is-active {
|
||||||
|
background-color: #3273dc;
|
||||||
|
color: #fff; }
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
background-color: #dbdbdb;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0.5rem 0; }
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
position: relative; }
|
||||||
|
.autocomplete .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%; }
|
||||||
|
.autocomplete .dropdown-menu.is-opened-top {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%; }
|
||||||
|
.autocomplete .dropdown-content {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 200px; }
|
||||||
|
.autocomplete .dropdown-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; }
|
||||||
|
.autocomplete .dropdown-item.is-hovered {
|
||||||
|
background: whitesmoke;
|
||||||
|
color: #0a0a0a; }
|
||||||
|
.autocomplete .dropdown-item.is-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed; }
|
||||||
|
.autocomplete.is-small {
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.75rem; }
|
||||||
|
.autocomplete.is-medium {
|
||||||
|
font-size: 1.25rem; }
|
||||||
|
.autocomplete.is-large {
|
||||||
|
font-size: 1.5rem; }
|
||||||
|
|
||||||
/*! bulma.io v0.7.5 | MIT License | github.com/jgthms/bulma */
|
/*! bulma.io v0.7.5 | MIT License | github.com/jgthms/bulma */
|
||||||
@keyframes spinAround {
|
@keyframes spinAround {
|
||||||
from {
|
from {
|
||||||
|
@ -7177,6 +7268,9 @@ label.panel-block {
|
||||||
.is-borderless {
|
.is-borderless {
|
||||||
border: none; }
|
border: none; }
|
||||||
|
|
||||||
|
.has-text-nowrap {
|
||||||
|
white-space: nowrap; }
|
||||||
|
|
||||||
.has-background-transparent {
|
.has-background-transparent {
|
||||||
background-color: transparent; }
|
background-color: transparent; }
|
||||||
|
|
||||||
|
@ -7208,6 +7302,18 @@ a.navbar-item.is-active {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
padding: 0em; }
|
padding: 0em; }
|
||||||
|
|
||||||
|
.navbar.toolbar {
|
||||||
|
margin: 1em 0em;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-bottom: 1em; }
|
||||||
|
.navbar.toolbar .title {
|
||||||
|
padding-right: 2em;
|
||||||
|
margin-right: 1em;
|
||||||
|
border-right: 1px #b5b5b5 solid;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #7a7a7a;
|
||||||
|
font-weight: 300; }
|
||||||
|
|
||||||
.card .title {
|
.card .title {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
@ -7230,18 +7336,6 @@ a.navbar-item.is-active {
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
font-size: 0.8em; }
|
font-size: 0.8em; }
|
||||||
|
|
||||||
.filters {
|
|
||||||
margin: 1em 0em;
|
|
||||||
background-color: transparent;
|
|
||||||
margin-bottom: 1em; }
|
|
||||||
.filters .title {
|
|
||||||
padding-right: 2em;
|
|
||||||
margin-right: 1em;
|
|
||||||
border-right: 1px #b5b5b5 solid;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: #7a7a7a;
|
|
||||||
font-weight: 300; }
|
|
||||||
|
|
||||||
.page > .cover {
|
.page > .cover {
|
||||||
float: right;
|
float: right;
|
||||||
max-width: 45%; }
|
max-width: 45%; }
|
||||||
|
|
|
@ -218,11 +218,47 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _nod
|
||||||
/*!******************************!*\
|
/*!******************************!*\
|
||||||
!*** ./assets/public/app.js ***!
|
!*** ./assets/public/app.js ***!
|
||||||
\******************************/
|
\******************************/
|
||||||
/*! exports provided: app, default */
|
/*! exports provided: appBaseConfig, setAppConfig, getAppConfig, loadApp */
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"app\", function() { return app; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\n\nvar app = null;\n/* harmony default export */ __webpack_exports__[\"default\"] = (app);\n\nfunction loadApp() {\n app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]({\n el: '#app',\n delimiters: [ '[[', ']]' ],\n })\n}\n\nwindow.addEventListener('load', loadApp);\n\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
|
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/autocomplete.vue":
|
||||||
|
/*!****************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue ***!
|
||||||
|
\****************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./autocomplete.vue?vue&type=template&id=70936760& */ \"./assets/public/autocomplete.vue?vue&type=template&id=70936760&\");\n/* harmony import */ var _autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./autocomplete.vue?vue&type=script&lang=js& */ \"./assets/public/autocomplete.vue?vue&type=script&lang=js&\");\n/* empty/unused harmony star reexport *//* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/public/autocomplete.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./assets/public/autocomplete.vue?vue&type=script&lang=js&":
|
||||||
|
/*!*****************************************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue?vue&type=script&lang=js& ***!
|
||||||
|
\*****************************************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=script&lang=js&\");\n/* empty/unused harmony star reexport */ /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[\"default\"]); \n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./assets/public/autocomplete.vue?vue&type=template&id=70936760&":
|
||||||
|
/*!***********************************************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue?vue&type=template&id=70936760& ***!
|
||||||
|
\***********************************************************************/
|
||||||
|
/*! exports provided: render, staticRenderFns */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete.vue?vue&type=template&id=70936760& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=template&id=70936760&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
@ -234,7 +270,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/* harmony import */ var _autocomplete_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./autocomplete.vue */ \"./assets/public/autocomplete.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_5__[\"default\"])\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-autocomplete', _autocomplete_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n // main application\n app: null,\n\n // main application config\n appConfig: {},\n\n // player application\n playerApp: null,\n\n // player component\n get player() {\n return this.playerApp && this.playerApp.$refs.player\n }\n};\n\n\nObject(_app__WEBPACK_IMPORTED_MODULE_3__[\"loadApp\"])({el: '#player'}).then(app => { window.aircox.playerApp = app },\n () => undefined)\nObject(_app__WEBPACK_IMPORTED_MODULE_3__[\"loadApp\"])(() => window.aircox.appConfig ).then(app => { window.aircox.app = app },\n () => undefined)\n\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
@ -309,6 +345,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n//\n//\n//\n//\n//\n//\n\n\nc
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./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& ***!
|
||||||
|
\*******************************************************************************************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lodash/debounce */ \"./node_modules/lodash/debounce.js\");\n/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash_debounce__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy/dist/components/autocomplete */ \"./node_modules/buefy/dist/components/autocomplete/index.js\");\n/* harmony import */ var buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n props: {\n url: String,\n model: Function,\n placeholder: String,\n field: {type: String, default: 'value'},\n count: {type: Number, count: 10},\n valueAttr: String,\n valueField: String,\n },\n\n data() {\n return {\n data: [],\n selected: null,\n isFetching: false,\n };\n },\n\n methods: {\n onSelect(option) {\n console.log('selected', option)\n vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"].set(this, 'selected', option);\n this.$emit('select', option);\n },\n\n fetch: lodash_debounce__WEBPACK_IMPORTED_MODULE_0___default()(function(query) {\n if(!query)\n return;\n\n this.isFetching = true;\n this.model.fetchAll(this.url.replace('${query}', query))\n .then(data => {\n this.data = data;\n this.isFetching = false;\n }, data => { this.isFetching = false; Promise.reject(data) })\n }),\n },\n\n components: {\n Autocomplete: buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__[\"Autocomplete\"],\n },\n});\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?./node_modules/vue-loader/lib??vue-loader-options");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=script&lang=js&":
|
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=script&lang=js&":
|
||||||
/*!*************************************************************************************************************!*\
|
/*!*************************************************************************************************************!*\
|
||||||
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=script&lang=js& ***!
|
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=script&lang=js& ***!
|
||||||
|
@ -333,6 +381,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=template&id=70936760&":
|
||||||
|
/*!*****************************************************************************************************************************************************************************************************!*\
|
||||||
|
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/autocomplete.vue?vue&type=template&id=70936760& ***!
|
||||||
|
\*****************************************************************************************************************************************************************************************************/
|
||||||
|
/*! exports provided: render, staticRenderFns */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n { staticClass: \"control\" },\n [\n _c(\"Autocomplete\", {\n ref: \"autocomplete\",\n attrs: {\n data: _vm.data,\n placeholder: _vm.placeholder,\n field: _vm.field,\n loading: _vm.isFetching,\n \"open-on-focus\": \"\"\n },\n on: {\n typing: _vm.fetch,\n select: function(object) {\n return _vm.onSelect(object)\n }\n }\n }),\n _vm._v(\" \"),\n _vm.valueField\n ? _c(\"input\", {\n ref: \"value\",\n attrs: { type: \"hidden\", name: _vm.valueField },\n domProps: {\n value:\n _vm.selected && _vm.selected[_vm.valueAttr || _vm.valueField]\n }\n })\n : _vm._e()\n ],\n 1\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=template&id=42a56ec9&":
|
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=template&id=42a56ec9&":
|
||||||
/*!***********************************************************************************************************************************************************************************************!*\
|
/*!***********************************************************************************************************************************************************************************************!*\
|
||||||
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=template&id=42a56ec9& ***!
|
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=template&id=42a56ec9& ***!
|
||||||
|
|
|
@ -189,6 +189,97 @@
|
||||||
fieldset[disabled] .pagination-ellipsis {
|
fieldset[disabled] .pagination-ellipsis {
|
||||||
cursor: not-allowed; }
|
cursor: not-allowed; }
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top; }
|
||||||
|
.dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu {
|
||||||
|
display: block; }
|
||||||
|
.dropdown.is-right .dropdown-menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0; }
|
||||||
|
.dropdown.is-up .dropdown-menu {
|
||||||
|
bottom: 100%;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
padding-top: initial;
|
||||||
|
top: auto; }
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
min-width: 12rem;
|
||||||
|
padding-top: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 20; }
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.5rem; }
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: #4a4a4a;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
a.dropdown-item,
|
||||||
|
button.dropdown-item {
|
||||||
|
padding-right: 3rem;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%; }
|
||||||
|
a.dropdown-item:hover,
|
||||||
|
button.dropdown-item:hover {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
color: #0a0a0a; }
|
||||||
|
a.dropdown-item.is-active,
|
||||||
|
button.dropdown-item.is-active {
|
||||||
|
background-color: #3273dc;
|
||||||
|
color: #fff; }
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
background-color: #dbdbdb;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0.5rem 0; }
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
position: relative; }
|
||||||
|
.autocomplete .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%; }
|
||||||
|
.autocomplete .dropdown-menu.is-opened-top {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%; }
|
||||||
|
.autocomplete .dropdown-content {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 200px; }
|
||||||
|
.autocomplete .dropdown-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; }
|
||||||
|
.autocomplete .dropdown-item.is-hovered {
|
||||||
|
background: whitesmoke;
|
||||||
|
color: #0a0a0a; }
|
||||||
|
.autocomplete .dropdown-item.is-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed; }
|
||||||
|
.autocomplete.is-small {
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.75rem; }
|
||||||
|
.autocomplete.is-medium {
|
||||||
|
font-size: 1.25rem; }
|
||||||
|
.autocomplete.is-large {
|
||||||
|
font-size: 1.5rem; }
|
||||||
|
|
||||||
/*! bulma.io v0.7.5 | MIT License | github.com/jgthms/bulma */
|
/*! bulma.io v0.7.5 | MIT License | github.com/jgthms/bulma */
|
||||||
@keyframes spinAround {
|
@keyframes spinAround {
|
||||||
from {
|
from {
|
||||||
|
@ -7159,6 +7250,9 @@ label.panel-block {
|
||||||
.is-borderless {
|
.is-borderless {
|
||||||
border: none; }
|
border: none; }
|
||||||
|
|
||||||
|
.has-text-nowrap {
|
||||||
|
white-space: nowrap; }
|
||||||
|
|
||||||
.has-background-transparent {
|
.has-background-transparent {
|
||||||
background-color: transparent; }
|
background-color: transparent; }
|
||||||
|
|
||||||
|
@ -7190,6 +7284,18 @@ a.navbar-item.is-active {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
padding: 0em; }
|
padding: 0em; }
|
||||||
|
|
||||||
|
.navbar.toolbar {
|
||||||
|
margin: 1em 0em;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-bottom: 1em; }
|
||||||
|
.navbar.toolbar .title {
|
||||||
|
padding-right: 2em;
|
||||||
|
margin-right: 1em;
|
||||||
|
border-right: 1px #b5b5b5 solid;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #7a7a7a;
|
||||||
|
font-weight: 300; }
|
||||||
|
|
||||||
.card .title {
|
.card .title {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
@ -7212,18 +7318,6 @@ a.navbar-item.is-active {
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
font-size: 0.8em; }
|
font-size: 0.8em; }
|
||||||
|
|
||||||
.filters {
|
|
||||||
margin: 1em 0em;
|
|
||||||
background-color: transparent;
|
|
||||||
margin-bottom: 1em; }
|
|
||||||
.filters .title {
|
|
||||||
padding-right: 2em;
|
|
||||||
margin-right: 1em;
|
|
||||||
border-right: 1px #b5b5b5 solid;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: #7a7a7a;
|
|
||||||
font-weight: 300; }
|
|
||||||
|
|
||||||
.page > .cover {
|
.page > .cover {
|
||||||
float: right;
|
float: right;
|
||||||
max-width: 45%; }
|
max-width: 45%; }
|
||||||
|
|
|
@ -159,11 +159,47 @@
|
||||||
/*!******************************!*\
|
/*!******************************!*\
|
||||||
!*** ./assets/public/app.js ***!
|
!*** ./assets/public/app.js ***!
|
||||||
\******************************/
|
\******************************/
|
||||||
/*! exports provided: app, default */
|
/*! exports provided: appBaseConfig, setAppConfig, getAppConfig, loadApp */
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"app\", function() { return app; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\n\nvar app = null;\n/* harmony default export */ __webpack_exports__[\"default\"] = (app);\n\nfunction loadApp() {\n app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]({\n el: '#app',\n delimiters: [ '[[', ']]' ],\n })\n}\n\nwindow.addEventListener('load', loadApp);\n\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
|
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/autocomplete.vue":
|
||||||
|
/*!****************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue ***!
|
||||||
|
\****************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./autocomplete.vue?vue&type=template&id=70936760& */ \"./assets/public/autocomplete.vue?vue&type=template&id=70936760&\");\n/* harmony import */ var _autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./autocomplete.vue?vue&type=script&lang=js& */ \"./assets/public/autocomplete.vue?vue&type=script&lang=js&\");\n/* empty/unused harmony star reexport *//* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/public/autocomplete.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./assets/public/autocomplete.vue?vue&type=script&lang=js&":
|
||||||
|
/*!*****************************************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue?vue&type=script&lang=js& ***!
|
||||||
|
\*****************************************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=script&lang=js&\");\n/* empty/unused harmony star reexport */ /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[\"default\"]); \n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./assets/public/autocomplete.vue?vue&type=template&id=70936760&":
|
||||||
|
/*!***********************************************************************!*\
|
||||||
|
!*** ./assets/public/autocomplete.vue?vue&type=template&id=70936760& ***!
|
||||||
|
\***********************************************************************/
|
||||||
|
/*! exports provided: render, staticRenderFns */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete.vue?vue&type=template&id=70936760& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=template&id=70936760&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_vue_vue_type_template_id_70936760___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
@ -175,7 +211,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _liveInfo__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./liveInfo */ \"./assets/public/liveInfo.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n app: _app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n LiveInfo: _liveInfo__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n}\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./assets/public/app.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./styles.scss */ \"./assets/public/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./player.vue */ \"./assets/public/player.vue\");\n/* harmony import */ var _autocomplete_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./autocomplete.vue */ \"./assets/public/autocomplete.vue\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n\n\n\n\n//-- aircox\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_5__[\"default\"])\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-autocomplete', _autocomplete_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"])\n\n\nwindow.aircox = {\n // main application\n app: null,\n\n // main application config\n appConfig: {},\n\n // player application\n playerApp: null,\n\n // player component\n get player() {\n return this.playerApp && this.playerApp.$refs.player\n }\n};\n\n\nObject(_app__WEBPACK_IMPORTED_MODULE_3__[\"loadApp\"])({el: '#player'}).then(app => { window.aircox.playerApp = app },\n () => undefined)\nObject(_app__WEBPACK_IMPORTED_MODULE_3__[\"loadApp\"])(() => window.aircox.appConfig ).then(app => { window.aircox.app = app },\n () => undefined)\n\n\n\n\n//# sourceURL=webpack:///./assets/public/index.js?");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
@ -238,6 +274,18 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./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& ***!
|
||||||
|
\*******************************************************************************************************************/
|
||||||
|
/*! exports provided: default */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lodash/debounce */ \"./node_modules/lodash/debounce.js\");\n/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash_debounce__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy/dist/components/autocomplete */ \"./node_modules/buefy/dist/components/autocomplete/index.js\");\n/* harmony import */ var buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n props: {\n url: String,\n model: Function,\n placeholder: String,\n field: {type: String, default: 'value'},\n count: {type: Number, count: 10},\n valueAttr: String,\n valueField: String,\n },\n\n data() {\n return {\n data: [],\n selected: null,\n isFetching: false,\n };\n },\n\n methods: {\n onSelect(option) {\n console.log('selected', option)\n vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"].set(this, 'selected', option);\n this.$emit('select', option);\n },\n\n fetch: lodash_debounce__WEBPACK_IMPORTED_MODULE_0___default()(function(query) {\n if(!query)\n return;\n\n this.isFetching = true;\n this.model.fetchAll(this.url.replace('${query}', query))\n .then(data => {\n this.data = data;\n this.isFetching = false;\n }, data => { this.isFetching = false; Promise.reject(data) })\n }),\n },\n\n components: {\n Autocomplete: buefy_dist_components_autocomplete__WEBPACK_IMPORTED_MODULE_1__[\"Autocomplete\"],\n },\n});\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?./node_modules/vue-loader/lib??vue-loader-options");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=script&lang=js&":
|
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=script&lang=js&":
|
||||||
/*!*************************************************************************************************************!*\
|
/*!*************************************************************************************************************!*\
|
||||||
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=script&lang=js& ***!
|
!*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=script&lang=js& ***!
|
||||||
|
@ -250,6 +298,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/autocomplete.vue?vue&type=template&id=70936760&":
|
||||||
|
/*!*****************************************************************************************************************************************************************************************************!*\
|
||||||
|
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/autocomplete.vue?vue&type=template&id=70936760& ***!
|
||||||
|
\*****************************************************************************************************************************************************************************************************/
|
||||||
|
/*! exports provided: render, staticRenderFns */
|
||||||
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n { staticClass: \"control\" },\n [\n _c(\"Autocomplete\", {\n ref: \"autocomplete\",\n attrs: {\n data: _vm.data,\n placeholder: _vm.placeholder,\n field: _vm.field,\n loading: _vm.isFetching,\n \"open-on-focus\": \"\"\n },\n on: {\n typing: _vm.fetch,\n select: function(object) {\n return _vm.onSelect(object)\n }\n }\n }),\n _vm._v(\" \"),\n _vm.valueField\n ? _c(\"input\", {\n ref: \"value\",\n attrs: { type: \"hidden\", name: _vm.valueField },\n domProps: {\n value:\n _vm.selected && _vm.selected[_vm.valueAttr || _vm.valueField]\n }\n })\n : _vm._e()\n ],\n 1\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/public/autocomplete.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=template&id=42a56ec9&":
|
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/public/player.vue?vue&type=template&id=42a56ec9&":
|
||||||
/*!***********************************************************************************************************************************************************************************************!*\
|
/*!***********************************************************************************************************************************************************************************************!*\
|
||||||
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=template&id=42a56ec9& ***!
|
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/public/player.vue?vue&type=template&id=42a56ec9& ***!
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,9 +1,10 @@
|
||||||
{% load i18n static %}<!DOCTYPE html>
|
{% load i18n static aircox_admin %}<!DOCTYPE html>
|
||||||
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}">
|
<link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}">
|
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}">
|
<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}">
|
||||||
|
@ -67,7 +68,10 @@
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a href="#" class="navbar-link">{% trans "Tools" %}</a>
|
<a href="#" class="navbar-link">{% trans "Tools" %}</a>
|
||||||
<div class="navbar-dropdown is-boxed is-right">
|
<div class="navbar-dropdown is-boxed is-right">
|
||||||
<a href="{% url 'admin:tools-stats' %}" class="navbar-item">{% trans "Statistics" %}</a>
|
{% get_admin_tools as admin_tools %}
|
||||||
|
{% for label, url in admin_tools %}
|
||||||
|
<a href="{{ url }}" class="navbar-item">{{ label }}</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -121,7 +125,7 @@
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
||||||
{% block pretitle %}{% endblock %}
|
{% block pretitle %}{% endblock %}
|
||||||
{% block content_title %}{% if title %}<h1 class="subtitle is-3">{{ title }}</h1>{% endif %}{% endblock %}
|
{% block content_title %}{% if title %}<h1 class="title is-3">{{ title }}</h1>{% endif %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% block object-tools %}{% endblock %}
|
{% block object-tools %}{% endblock %}
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|
|
@ -93,7 +93,7 @@ Blocks:
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% if has_filters %}
|
{% if has_filters %}
|
||||||
<nav class="navbar filters"
|
<nav class="navbar toolbar"
|
||||||
aria-label="{% trans "list filters" %}">
|
aria-label="{% trans "list filters" %}">
|
||||||
{% block filters %}{% endblock %}
|
{% block filters %}{% endblock %}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -132,8 +132,8 @@ Blocks:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
{% include "aircox/player.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="player">{% include "aircox/player.html" %}</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,9 @@ Context:
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h4 class="title is-4">{% trans "Last publications" %}</h4>
|
<h4 class="title is-4">{% trans "Last publications" %}</h4>
|
||||||
|
{% with has_headline=True %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<a-player ref="player" src="{{ audio_streams.0 }}"
|
<a-player ref="player" src="{{ audio_streams.0 }}"
|
||||||
live-info-url="{% url "api-live" %}" :live-info-timeout="15"
|
live-info-url="{% url "api-live" %}" :live-info-timeout="20"
|
||||||
button-title="{% trans "Play or pause audio" %}">
|
button-title="{% trans "Play or pause audio" %}">
|
||||||
<template v-slot:sources>
|
<template v-slot:sources>
|
||||||
{% for stream in audio_streams %}
|
{% for stream in audio_streams %}
|
||||||
|
|
|
@ -12,12 +12,17 @@ __all__ = ['BaseAdminView', 'StatisticsView']
|
||||||
class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
||||||
title = ''
|
title = ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def station(self):
|
||||||
|
return self.request.station
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
return self.request.user.is_staff
|
return self.request.user.is_staff
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs.update(admin.site.each_context(self.request))
|
kwargs.update(admin.site.each_context(self.request))
|
||||||
kwargs.setdefault('title', self.title)
|
kwargs.setdefault('title', self.title)
|
||||||
|
kwargs.setdefault('station', self.station)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,5 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class AircoxStreamerConfig(AppConfig):
|
class AircoxStreamerConfig(AppConfig):
|
||||||
name = 'aircox_streamer'
|
name = 'aircox_streamer'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,8 @@ class BaseMetadata:
|
||||||
""" Request uri """
|
""" Request uri """
|
||||||
status = None
|
status = None
|
||||||
""" Current playing status """
|
""" Current playing status """
|
||||||
|
request_status = None
|
||||||
|
""" Requests' status """
|
||||||
air_time = None
|
air_time = None
|
||||||
""" Launch datetime """
|
""" Launch datetime """
|
||||||
|
|
||||||
|
@ -58,10 +60,25 @@ class BaseMetadata:
|
||||||
return self.status == 'playing'
|
return self.status == 'playing'
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
data = self.controller.set('request.metadata ', self.rid, parse=True)
|
data = self.controller.send('request.metadata ', self.rid, parse=True)
|
||||||
if data:
|
if data:
|
||||||
self.validate(data)
|
self.validate(data)
|
||||||
|
|
||||||
|
def validate_status(self, status):
|
||||||
|
on_air = self.controller.source
|
||||||
|
if on_air and status == 'playing' and (on_air == self or
|
||||||
|
on_air.rid == self.rid):
|
||||||
|
return 'playing'
|
||||||
|
elif status == 'playing':
|
||||||
|
return 'paused'
|
||||||
|
else:
|
||||||
|
return 'stopped'
|
||||||
|
|
||||||
|
def validate_air_time(self, air_time):
|
||||||
|
if air_time:
|
||||||
|
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
||||||
|
return local_tz.localize(air_time)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""
|
"""
|
||||||
Validate provided data and set as attribute (must already be
|
Validate provided data and set as attribute (must already be
|
||||||
|
@ -72,12 +89,9 @@ class BaseMetadata:
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
self.uri = data.get('initial_uri')
|
self.uri = data.get('initial_uri')
|
||||||
|
|
||||||
air_time = data.get('on_air')
|
self.air_time = self.validate_air_time(data.get('on_air'))
|
||||||
if air_time:
|
self.status = self.validate_status(data.get('status'))
|
||||||
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
self.request_status = data.get('status')
|
||||||
self.air_time = local_tz.localize(air_time)
|
|
||||||
else:
|
|
||||||
self.air_time = None
|
|
||||||
|
|
||||||
|
|
||||||
class Request(BaseMetadata):
|
class Request(BaseMetadata):
|
||||||
|
@ -142,6 +156,14 @@ class Streamer:
|
||||||
logger.debug('process died with return code %s' % returncode)
|
logger.debug('process died with return code %s' % returncode)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlists(self):
|
||||||
|
return (s for s in self.sources if isinstance(s, PlaylistSource))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queues(self):
|
||||||
|
return (s for s in self.sources if isinstance(s, QueueSource))
|
||||||
|
|
||||||
# Sources and config ###############################################
|
# Sources and config ###############################################
|
||||||
def send(self, *args, **kwargs):
|
def send(self, *args, **kwargs):
|
||||||
return self.connector.send(*args, **kwargs) or ''
|
return self.connector.send(*args, **kwargs) or ''
|
||||||
|
@ -180,12 +202,11 @@ class Streamer:
|
||||||
source.fetch()
|
source.fetch()
|
||||||
|
|
||||||
# request.on_air is not ordered: we need to do it manually
|
# request.on_air is not ordered: we need to do it manually
|
||||||
if self.dealer.is_playing:
|
self.source = next(iter(sorted(
|
||||||
self.source = self.dealer
|
(source for source in self.sources
|
||||||
return
|
if source.request_status == 'playing' and source.air_time),
|
||||||
|
key=lambda o: o.air_time, reverse=True
|
||||||
self.source = next((source for source in self.sources
|
)), None)
|
||||||
if source.is_playing), None)
|
|
||||||
|
|
||||||
# Process ##########################################################
|
# Process ##########################################################
|
||||||
def get_process_args(self):
|
def get_process_args(self):
|
||||||
|
@ -241,15 +262,12 @@ class Source(BaseMetadata):
|
||||||
""" source id """
|
""" source id """
|
||||||
remaining = 0.0
|
remaining = 0.0
|
||||||
""" remaining time """
|
""" remaining time """
|
||||||
|
status = 'stopped'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def station(self):
|
def station(self):
|
||||||
return self.controller.station
|
return self.controller.station
|
||||||
|
|
||||||
# @property
|
|
||||||
# def is_on_air(self):
|
|
||||||
# return self.rid is not None and self.rid in self.controller.on_air
|
|
||||||
|
|
||||||
def __init__(self, controller=None, id=None, *args, **kwargs):
|
def __init__(self, controller=None, id=None, *args, **kwargs):
|
||||||
super().__init__(controller, *args, **kwargs)
|
super().__init__(controller, *args, **kwargs)
|
||||||
self.id = id
|
self.id = id
|
||||||
|
@ -258,9 +276,12 @@ class Source(BaseMetadata):
|
||||||
""" Synchronize what should be synchronized """
|
""" Synchronize what should be synchronized """
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
|
try:
|
||||||
data = self.controller.send(self.id, '.remaining')
|
data = self.controller.send(self.id, '.remaining')
|
||||||
if data:
|
if data:
|
||||||
self.remaining = float(data)
|
self.remaining = float(data)
|
||||||
|
except ValueError:
|
||||||
|
self.remaining = None
|
||||||
|
|
||||||
data = self.controller.send(self.id, '.get', parse=True)
|
data = self.controller.send(self.id, '.get', parse=True)
|
||||||
if data:
|
if data:
|
||||||
|
@ -332,12 +353,9 @@ class PlaylistSource(Source):
|
||||||
class QueueSource(Source):
|
class QueueSource(Source):
|
||||||
queue = None
|
queue = None
|
||||||
""" Source's queue (excluded on_air request) """
|
""" Source's queue (excluded on_air request) """
|
||||||
as_requests = False
|
|
||||||
""" If True, queue is a list of Request """
|
|
||||||
|
|
||||||
def __init__(self, *args, queue_metadata=False, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.queue_metadata = queue_metadata
|
|
||||||
|
|
||||||
def push(self, *paths):
|
def push(self, *paths):
|
||||||
""" Add the provided paths to source's play queue """
|
""" Add the provided paths to source's play queue """
|
||||||
|
@ -346,13 +364,19 @@ class QueueSource(Source):
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
super().fetch()
|
super().fetch()
|
||||||
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
queue = self.controller.send(self.id, '_queue.queue').strip()
|
||||||
if not self.as_requests:
|
if not queue:
|
||||||
self.queue = queue
|
self.queue = []
|
||||||
return
|
return
|
||||||
|
|
||||||
self.queue = [Request(self.controller, rid) for rid in queue]
|
self.queue = queue.split(' ')
|
||||||
for request in self.queue:
|
|
||||||
|
@property
|
||||||
|
def requests(self):
|
||||||
|
""" Queue as requests metadata """
|
||||||
|
requests = [Request(self.controller, rid) for rid in self.queue]
|
||||||
|
for request in requests:
|
||||||
request.fetch()
|
request.fetch()
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ from django.utils import timezone as tz
|
||||||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||||
from aircox.utils import date_range
|
from aircox.utils import date_range
|
||||||
|
|
||||||
from aircox_streamer.liquidsoap import Streamer, PlaylistSource
|
from aircox_streamer.controllers import Streamer
|
||||||
|
|
||||||
|
|
||||||
# force using UTC
|
# force using UTC
|
||||||
|
@ -246,8 +246,7 @@ class Monitor:
|
||||||
|
|
||||||
self.sync_next = now + tz.timedelta(minutes=self.sync_timeout)
|
self.sync_next = now + tz.timedelta(minutes=self.sync_timeout)
|
||||||
|
|
||||||
for source in self.streamer.sources:
|
for source in self.streamer.playlists:
|
||||||
if isinstance(source, PlaylistSource):
|
|
||||||
source.sync()
|
source.sync()
|
||||||
|
|
||||||
|
|
||||||
|
@ -291,10 +290,8 @@ class Command (BaseCommand):
|
||||||
)
|
)
|
||||||
# TODO: sync-timeout, cancel-timeout
|
# TODO: sync-timeout, cancel-timeout
|
||||||
|
|
||||||
def handle(self, *args,
|
def handle(self, *args, config=None, run=None, monitor=None, station=[],
|
||||||
config=None, run=None, monitor=None,
|
delay=1000, timeout=600, **options):
|
||||||
station=[], delay=1000, timeout=600,
|
|
||||||
**options):
|
|
||||||
stations = Station.objects.filter(name__in=station) if station else \
|
stations = Station.objects.filter(name__in=station) if station else \
|
||||||
Station.objects.all()
|
Station.objects.all()
|
||||||
streamers = [Streamer(station) for station in stations]
|
streamers = [Streamer(station) for station in stations]
|
||||||
|
|
|
@ -1,24 +1,35 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .controllers import QueueSource, PlaylistSource
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['RequestSerializer', 'StreamerSerializer', 'SourceSerializer',
|
__all__ = ['RequestSerializer', 'StreamerSerializer', 'SourceSerializer',
|
||||||
'PlaylistSerializer', 'QueueSourceSerializer']
|
'PlaylistSerializer', 'QueueSourceSerializer']
|
||||||
# TODO: use models' serializers
|
# TODO: use models' serializers
|
||||||
|
|
||||||
|
|
||||||
class BaseMetadataSerializer(serializers.Serializer):
|
class BaseSerializer(serializers.Serializer):
|
||||||
|
url_ = serializers.SerializerMethodField('get_url')
|
||||||
|
url_name = None
|
||||||
|
|
||||||
|
def get_url(self, obj, **kwargs):
|
||||||
|
if not obj or not self.url_name:
|
||||||
|
return
|
||||||
|
kwargs.setdefault('pk', getattr(obj, 'id', None))
|
||||||
|
return reverse(self.url_name, kwargs=kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMetadataSerializer(BaseSerializer):
|
||||||
rid = serializers.IntegerField()
|
rid = serializers.IntegerField()
|
||||||
air_time = serializers.DateTimeField()
|
air_time = serializers.DateTimeField()
|
||||||
uri = serializers.CharField()
|
uri = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class RequestSerializer(serializers.Serializer):
|
class RequestSerializer(BaseMetadataSerializer):
|
||||||
title = serializers.CharField()
|
title = serializers.CharField(required=False)
|
||||||
artist = serializers.CharField()
|
artist = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class StreamerSerializer(serializers.Serializer):
|
|
||||||
station = serializers.CharField(source='station.title')
|
|
||||||
|
|
||||||
|
|
||||||
class SourceSerializer(BaseMetadataSerializer):
|
class SourceSerializer(BaseMetadataSerializer):
|
||||||
|
@ -27,14 +38,34 @@ class SourceSerializer(BaseMetadataSerializer):
|
||||||
rid = serializers.IntegerField()
|
rid = serializers.IntegerField()
|
||||||
air_time = serializers.DateTimeField()
|
air_time = serializers.DateTimeField()
|
||||||
status = serializers.CharField()
|
status = serializers.CharField()
|
||||||
|
remaining = serializers.FloatField()
|
||||||
|
|
||||||
|
def get_url(self, obj, **kwargs):
|
||||||
|
kwargs['station_pk'] = obj.station.pk
|
||||||
|
return super().get_url(obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PlaylistSerializer(SourceSerializer):
|
class PlaylistSerializer(SourceSerializer):
|
||||||
program = serializers.CharField(source='program.title')
|
program = serializers.CharField(source='program.title')
|
||||||
playlist = serializers.ListField(child=serializers.CharField())
|
|
||||||
|
|
||||||
|
url_name = 'admin:api:streamer-playlist-detail'
|
||||||
|
|
||||||
class QueueSourceSerializer(SourceSerializer):
|
class QueueSourceSerializer(SourceSerializer):
|
||||||
queue = serializers.ListField(child=RequestSerializer())
|
queue = serializers.ListField(child=RequestSerializer(), source='requests')
|
||||||
|
|
||||||
|
url_name = 'admin:api:streamer-queue-detail'
|
||||||
|
|
||||||
|
|
||||||
|
class StreamerSerializer(BaseSerializer):
|
||||||
|
id = serializers.IntegerField(source='station.pk')
|
||||||
|
name = serializers.CharField(source='station.name')
|
||||||
|
source = serializers.CharField(source='source.id', required=False)
|
||||||
|
playlists = serializers.ListField(child=PlaylistSerializer())
|
||||||
|
queues = serializers.ListField(child=QueueSourceSerializer())
|
||||||
|
|
||||||
|
url_name = 'admin:api:streamer-detail'
|
||||||
|
|
||||||
|
def get_url(self, obj, **kwargs):
|
||||||
|
kwargs['pk'] = obj.station.pk
|
||||||
|
return super().get_url(obj, **kwargs)
|
||||||
|
|
||||||
|
|
121
aircox_streamer/templates/aircox_streamer/source_item.html
Normal file
121
aircox_streamer/templates/aircox_streamer/source_item.html
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<section class="box"><div class="columns is-desktop">
|
||||||
|
<div class="column">
|
||||||
|
<h5 class='title is-5' :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
|
||||||
|
<span>
|
||||||
|
<span v-if="source.isPlaying" class="fas fa-play"></span>
|
||||||
|
<span v-else-if="source.isPaused" class="fas fa-pause"></span>
|
||||||
|
</span>
|
||||||
|
[[ source.id ]]
|
||||||
|
|
||||||
|
<small v-if="source.isPaused || source.isPlaying">(-[[ source.remainingString ]])</small>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="button" @click="source.sync()"
|
||||||
|
title="{% trans "Synchronize source with Liquidsoap" %}">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fas fa-sync"></span>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Synchronise" %}</span>
|
||||||
|
</button>
|
||||||
|
<button class="button" @click="source.restart()"
|
||||||
|
title="{% trans "Restart current track" %}">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fas fa-step-backward"></span>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Restart" %}</span>
|
||||||
|
</button>
|
||||||
|
<button class="button" @click="source.skip()"
|
||||||
|
title="{% trans "Skip current file" %}">
|
||||||
|
<span>{% trans "Skip" %}</span>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fas fa-step-forward"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="source.isQueue">
|
||||||
|
<hr>
|
||||||
|
<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}"
|
||||||
|
class="is-fullwidth"
|
||||||
|
:model="Sound" field="name" value-field="sound_id" value-attr="id"
|
||||||
|
{# FIXME dirty hack awaiting the vue component #}
|
||||||
|
placeholder="{% trans "Select a sound" %}"></a-autocomplete>
|
||||||
|
<p class="help">
|
||||||
|
{% trans "Add a sound to the queue (queue may start playing)" %}
|
||||||
|
</p>
|
||||||
|
{# TODO: help text about how it works #}
|
||||||
|
</div>
|
||||||
|
<div class="column control is-one-fifth">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<span class="icon">
|
||||||
|
<span class="fas fa-plus"></span>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Add" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="source.queue.length">
|
||||||
|
<h6 class="title is-6 is-marginless">{% trans "Sounds in queue" %}</h6>
|
||||||
|
<table class="table is-fullwidth"><tbody>
|
||||||
|
<tr v-for="[index, request] in source.queue.entries()">
|
||||||
|
<td :class="{'has-text-weight-semibold': index==0 }">
|
||||||
|
<span v-if="index==0" class="far fa-play-circle"></span>
|
||||||
|
<span>[[ request.data.uri ]]</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-two-fifths">
|
||||||
|
<h6 class="subtitle is-6 is-marginless">Metadata</h6>
|
||||||
|
<table class="table has-background-transparent">
|
||||||
|
<tbody>
|
||||||
|
<tr><th class="has-text-right has-text-nowrap">
|
||||||
|
{% trans "Status" %}
|
||||||
|
</th>
|
||||||
|
<td :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
|
||||||
|
<span v-if="source.isPlaying" class="fas fa-play"></span>
|
||||||
|
<span v-else-if="source.data.status" class="fas fa-pause"></span>
|
||||||
|
[[ source.data.status || "—" ]]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="source.data.air_time">
|
||||||
|
<th class="has-text-right has-text-nowrap">
|
||||||
|
{% trans "Air time" %}
|
||||||
|
</th><td>
|
||||||
|
<span class="far fa-clock"></span>
|
||||||
|
<time :datetime="source.date">
|
||||||
|
[[ source.data.air_time.toLocaleDateString() ]],
|
||||||
|
[[ source.data.air_time.toLocaleTimeString() ]]
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
<tr v-if="source.remaining">
|
||||||
|
<th class="has-text-right has-text-nowrap">
|
||||||
|
{% trans "Time left" %}
|
||||||
|
</th><td>
|
||||||
|
<span class="far fa-hourglass"></span>
|
||||||
|
[[ source.remainingString ]]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="source.data.uri">
|
||||||
|
<th class="has-text-right has-text-nowrap">
|
||||||
|
{% trans "Data source" %}
|
||||||
|
</th><td>
|
||||||
|
<span class="far fa-play-circle"></span>
|
||||||
|
<template v-if="source.data.uri.length > 64">...</template>[[ (source.data.uri && source.data.uri.slice(-64)) || '—' ]]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
|
39
aircox_streamer/templates/aircox_streamer/streamer.html
Normal file
39
aircox_streamer/templates/aircox_streamer/streamer.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
<script src="{% static "aircox/streamer.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}{{ block.super }}
|
||||||
|
<div id="app" data-api-url="{% url "admin:api:streamer-list" %}">
|
||||||
|
<div class="navbar toolbar">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<span class="navbar-item control">
|
||||||
|
<button class="button">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fas fa-sync"></span>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Reload" %}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="select navbar-item">
|
||||||
|
<select ref="selectStreamer" @change.native="selectStreamer" class="control"
|
||||||
|
title="{% trans "Select a station" %}"
|
||||||
|
aria-label="{% trans "Select a station" %}">
|
||||||
|
<option v-for="streamer of streamers" :value="streamer.id">[[ streamer.data.name ]]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="streamer">
|
||||||
|
<template v-for="source in sources">
|
||||||
|
{% include "aircox_streamer/source_item.html" %}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
22
aircox_streamer/urls.py
Normal file
22
aircox_streamer/urls.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from . import viewsets
|
||||||
|
from .views import StreamerAdminView
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.route_view('tools/streamer', StreamerAdminView.as_view(),
|
||||||
|
'tools-streamer', label=_('Streamer Monitor'))
|
||||||
|
|
||||||
|
streamer_prefix = 'streamer/(?P<station_pk>[0-9]+)/'
|
||||||
|
|
||||||
|
|
||||||
|
router = admin.site.router
|
||||||
|
router.register(streamer_prefix + 'playlist', viewsets.PlaylistSourceViewSet,
|
||||||
|
basename='streamer-playlist')
|
||||||
|
router.register(streamer_prefix + 'queue', viewsets.QueueSourceViewSet,
|
||||||
|
basename='streamer-queue')
|
||||||
|
router.register('streamer', viewsets.StreamerViewSet, basename='streamer')
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
from django.shortcuts import render
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from aircox.views.admin import BaseAdminView
|
||||||
|
|
||||||
|
|
||||||
|
class StreamerAdminView(BaseAdminView, TemplateView):
|
||||||
|
template_name = 'aircox_streamer/streamer.html'
|
||||||
|
title = _('Streamer Monitor')
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from aircox import controllers
|
from aircox.models import Sound, Station
|
||||||
from aircox.models import Station
|
from aircox.serializers import SoundSerializer
|
||||||
|
from . import controllers
|
||||||
from .serializers import *
|
from .serializers import *
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,16 +56,17 @@ class Streamers:
|
||||||
self.date = now + self.timeout
|
self.date = now + self.timeout
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
self.fetch()
|
|
||||||
return self.streamers.get(key, default)
|
return self.streamers.get(key, default)
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
self.fetch()
|
|
||||||
return self.streamers.values()
|
return self.streamers.values()
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.streamers[key]
|
return self.streamers[key]
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in self.streamers
|
||||||
|
|
||||||
|
|
||||||
streamers = Streamers()
|
streamers = Streamers()
|
||||||
|
|
||||||
|
@ -70,22 +75,25 @@ class BaseControllerAPIView(viewsets.ViewSet):
|
||||||
permission_classes = (IsAdminUser,)
|
permission_classes = (IsAdminUser,)
|
||||||
serializer = None
|
serializer = None
|
||||||
streamer = None
|
streamer = None
|
||||||
|
object = None
|
||||||
|
|
||||||
def get_streamer(self, pk=None):
|
def get_streamer(self, request, station_pk=None, **kwargs):
|
||||||
streamer = streamers.get(self.request.pk if pk is None else pk)
|
streamers.fetch()
|
||||||
if not streamer:
|
id = int(request.station.pk if station_pk is None else station_pk)
|
||||||
|
if id not in streamers:
|
||||||
raise Http404('station not found')
|
raise Http404('station not found')
|
||||||
return streamer
|
return streamers[id]
|
||||||
|
|
||||||
def get_serializer(self, obj, **kwargs):
|
def get_serializer(self, **kwargs):
|
||||||
return self.serializer(obj, **kwargs)
|
return self.serializer(self.object, **kwargs)
|
||||||
|
|
||||||
def serialize(self, obj, **kwargs):
|
def serialize(self, obj, **kwargs):
|
||||||
serializer = self.get_serializer(obj, **kwargs)
|
self.object = obj
|
||||||
|
serializer = self.get_serializer(**kwargs)
|
||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, station_pk=None, **kwargs):
|
||||||
self.streamer = self.get_streamer(request.station.pk)
|
self.streamer = self.get_streamer(request, station_pk, **kwargs)
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,10 +105,19 @@ class StreamerViewSet(BaseControllerAPIView):
|
||||||
serializer = StreamerSerializer
|
serializer = StreamerSerializer
|
||||||
|
|
||||||
def retrieve(self, request, pk=None):
|
def retrieve(self, request, pk=None):
|
||||||
return self.serialize(self.streamer)
|
return Response(self.serialize(self.streamer))
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request, pk=None):
|
||||||
return self.serialize(streamers.values(), many=True)
|
return Response({
|
||||||
|
'results': self.serialize(streamers.values(), many=True)
|
||||||
|
})
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, pk=None, **kwargs):
|
||||||
|
if pk is not None:
|
||||||
|
kwargs.setdefault('station_pk', pk)
|
||||||
|
self.streamer = self.get_streamer(request, **kwargs)
|
||||||
|
self.object = self.streamer
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SourceViewSet(BaseControllerAPIView):
|
class SourceViewSet(BaseControllerAPIView):
|
||||||
|
@ -108,38 +125,46 @@ class SourceViewSet(BaseControllerAPIView):
|
||||||
model = controllers.Source
|
model = controllers.Source
|
||||||
|
|
||||||
def get_sources(self):
|
def get_sources(self):
|
||||||
return (s for s in self.streamer.souces if isinstance(s, self.model))
|
return (s for s in self.streamer.sources if isinstance(s, self.model))
|
||||||
|
|
||||||
def get_source(self, pk):
|
def get_source(self, pk):
|
||||||
source = next((source for source in self.get_sources()
|
source = next((source for source in self.get_sources()
|
||||||
if source.pk == pk), None)
|
if source.id == pk), None)
|
||||||
if source is None:
|
if source is None:
|
||||||
raise Http404('source `%s` not found' % pk)
|
raise Http404('source `%s` not found' % pk)
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def retrieve(self, request, pk=None):
|
def retrieve(self, request, pk=None):
|
||||||
source = self.get_source(pk)
|
self.object = self.get_source(pk)
|
||||||
return self.serialize(source)
|
return Response(self.serialize())
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
return self.serialize(self.get_sources(), many=True)
|
return Response({
|
||||||
|
'results': self.serialize(self.get_sources(), many=True)
|
||||||
|
})
|
||||||
|
|
||||||
|
def _run(self, pk, action):
|
||||||
|
source = self.object = self.get_source(pk)
|
||||||
|
action(source)
|
||||||
|
source.fetch()
|
||||||
|
return Response(self.serialize(source))
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def sync(self, request, pk):
|
def sync(self, request, pk):
|
||||||
self.get_source(pk).sync()
|
return self._run(pk, lambda s: s.sync())
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def skip(self, request, pk):
|
def skip(self, request, pk):
|
||||||
self.get_source(pk).skip()
|
return self._run(pk, lambda s: s.skip())
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def restart(self, request, pk):
|
def restart(self, request, pk):
|
||||||
self.get_source(pk).restart()
|
return self._run(pk, lambda s: s.restart())
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def seek(self, request, pk):
|
def seek(self, request, pk):
|
||||||
count = request.POST['seek']
|
count = request.POST['seek']
|
||||||
self.get_source(pk).seek(count)
|
return self._run(pk, lambda s: s.seek(count))
|
||||||
|
|
||||||
|
|
||||||
class PlaylistSourceViewSet(SourceViewSet):
|
class PlaylistSourceViewSet(SourceViewSet):
|
||||||
|
@ -151,8 +176,26 @@ class QueueSourceViewSet(SourceViewSet):
|
||||||
serializer = QueueSourceSerializer
|
serializer = QueueSourceSerializer
|
||||||
model = controllers.QueueSource
|
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'])
|
@action(detail=True, methods=['POST'])
|
||||||
def push(self, request, pk):
|
def push(self, request, pk):
|
||||||
self.get_source(pk).push()
|
if not request.data.get('sound_id'):
|
||||||
|
raise ValidationError('missing "sound_id" POST data')
|
||||||
|
|
||||||
|
sound = get_object_or_404(self.get_sound_queryset(),
|
||||||
|
pk=request.data['sound_id'])
|
||||||
|
return self._run(
|
||||||
|
pk, lambda s: s.push(sound.path) if sound.path else None)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,53 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
|
||||||
export var app = null;
|
export const appBaseConfig = {
|
||||||
export default app;
|
|
||||||
|
|
||||||
function loadApp() {
|
|
||||||
app = new Vue({
|
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: [ '[[', ']]' ],
|
delimiters: ['[[', ']]'],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application config for the main application instance
|
||||||
|
*/
|
||||||
|
var appConfig = {};
|
||||||
|
|
||||||
|
export function setAppConfig(config) {
|
||||||
|
for(var member in appConfig) delete appConfig[member];
|
||||||
|
return Object.assign(appConfig, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppConfig(config) {
|
||||||
|
if(config instanceof Function)
|
||||||
|
config = config()
|
||||||
|
config = config == null ? appConfig : config;
|
||||||
|
return {...appBaseConfig, ...config}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Vue application at window 'load' event and return a Promise
|
||||||
|
* resolving to the created app.
|
||||||
|
*
|
||||||
|
* config: defaults to appConfig (checked when window is loaded)
|
||||||
|
*/
|
||||||
|
export function loadApp(config=null) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
try {
|
||||||
|
config = getAppConfig(config)
|
||||||
|
const el = document.querySelector(config.el)
|
||||||
|
if(!el) {
|
||||||
|
reject(`Error: missing element ${config.el}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(new Vue(config))
|
||||||
|
}
|
||||||
|
catch(error) { reject(error) }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', loadApp);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
63
assets/public/autocomplete.vue
Normal file
63
assets/public/autocomplete.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="control">
|
||||||
|
<Autocomplete ref="autocomplete" :data="data" :placeholder="placeholder" :field="field"
|
||||||
|
:loading="isFetching" open-on-focus
|
||||||
|
@typing="fetch" @select="object => onSelect(object)"
|
||||||
|
>
|
||||||
|
</Autocomplete>
|
||||||
|
<input v-if="valueField" ref="value" type="hidden" :name="valueField"
|
||||||
|
:value="selected && selected[valueAttr || valueField]" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
import {Autocomplete} from 'buefy/dist/components/autocomplete';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
model: Function,
|
||||||
|
placeholder: String,
|
||||||
|
field: {type: String, default: 'value'},
|
||||||
|
count: {type: Number, count: 10},
|
||||||
|
valueAttr: String,
|
||||||
|
valueField: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
selected: null,
|
||||||
|
isFetching: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSelect(option) {
|
||||||
|
console.log('selected', option)
|
||||||
|
Vue.set(this, 'selected', option);
|
||||||
|
this.$emit('select', option);
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch: debounce(function(query) {
|
||||||
|
if(!query)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isFetching = true;
|
||||||
|
this.model.fetchAll(this.url.replace('${query}', query))
|
||||||
|
.then(data => {
|
||||||
|
this.data = data;
|
||||||
|
this.isFetching = false;
|
||||||
|
}, data => { this.isFetching = false; Promise.reject(data) })
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Autocomplete,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
|
@ -10,18 +10,37 @@ import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||||
|
|
||||||
|
|
||||||
//-- aircox
|
//-- aircox
|
||||||
import app from './app';
|
import {appConfig, loadApp} from './app';
|
||||||
import LiveInfo from './liveInfo';
|
|
||||||
|
|
||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
import Player from './player.vue';
|
import Player from './player.vue';
|
||||||
|
import Autocomplete from './autocomplete.vue';
|
||||||
|
|
||||||
Vue.component('a-player', Player)
|
Vue.component('a-player', Player)
|
||||||
|
Vue.component('a-autocomplete', Autocomplete)
|
||||||
|
|
||||||
|
|
||||||
window.aircox = {
|
window.aircox = {
|
||||||
app: app,
|
// main application
|
||||||
LiveInfo: LiveInfo,
|
app: null,
|
||||||
}
|
|
||||||
|
// main application config
|
||||||
|
appConfig: {},
|
||||||
|
|
||||||
|
// player application
|
||||||
|
playerApp: null,
|
||||||
|
|
||||||
|
// player component
|
||||||
|
get player() {
|
||||||
|
return this.playerApp && this.playerApp.$refs.player
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
loadApp({el: '#player'}).then(app => { window.aircox.playerApp = app },
|
||||||
|
() => undefined)
|
||||||
|
loadApp(() => window.aircox.appConfig ).then(app => { window.aircox.app = app },
|
||||||
|
() => undefined)
|
||||||
|
|
||||||
|
|
||||||
|
|
118
assets/public/model.js
Normal file
118
assets/public/model.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
if(document.cookie && document.cookie !== '') {
|
||||||
|
const cookie = document.cookie.split(';')
|
||||||
|
.find(c => c.trim().startsWith(name + '='))
|
||||||
|
return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var csrfToken = null;
|
||||||
|
|
||||||
|
export function getCsrf() {
|
||||||
|
if(csrfToken === null)
|
||||||
|
csrfToken = getCookie('csrftoken')
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: move in another module for reuse
|
||||||
|
export default class Model {
|
||||||
|
constructor(data, {url=null}={}) {
|
||||||
|
this.commit(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getId(data) {
|
||||||
|
return data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOptions(options) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRFToken': getCsrf(),
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetch(url, options=null, initArgs=null) {
|
||||||
|
options = this.getOptions(options)
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => new this(d, {url: url, ...initArgs}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetchAll(url, options=null, initArgs=null) {
|
||||||
|
options = this.getOptions(options)
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if(!(data instanceof Array))
|
||||||
|
data = data.results;
|
||||||
|
data = data.map(d => new this(d, {baseUrl: url, ...initArgs}));
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from server.
|
||||||
|
*/
|
||||||
|
fetch(options) {
|
||||||
|
options = this.constructor.getOptions(options)
|
||||||
|
return fetch(this.url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => this.commit(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call API action on object.
|
||||||
|
*/
|
||||||
|
action(path, options, commit=false) {
|
||||||
|
options = this.constructor.getOptions(options)
|
||||||
|
const promise = fetch(this.url + path, options);
|
||||||
|
return commit ? promise.then(data => data.json())
|
||||||
|
.then(data => { this.commit(data); this.data })
|
||||||
|
: promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update instance's data with provided data. Return None
|
||||||
|
*/
|
||||||
|
commit(data) {
|
||||||
|
this.id = this.constructor.getId(data);
|
||||||
|
this.url = data.url_;
|
||||||
|
Vue.set(this, 'data', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return data as model with url prepent by `this.url`.
|
||||||
|
*/
|
||||||
|
asChild(model, data, prefix='') {
|
||||||
|
return new model(data, {baseUrl: `${this.url}${prefix}/`})
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildOf(attr, id) {
|
||||||
|
const index = this.data[attr].findIndex(o => o.id = id)
|
||||||
|
return index == -1 ? null : this.data[attr][index];
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateList(list=[], old=[]) {
|
||||||
|
return list.reduce((items, data) => {
|
||||||
|
const id = this.getId(data);
|
||||||
|
let [index, obj] = [old.findIndex(o => o.id == id), null];
|
||||||
|
if(index != -1) {
|
||||||
|
old[index].commit(data)
|
||||||
|
items.push(old[index]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
items.push(new this(data))
|
||||||
|
return items;
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
10
assets/public/sound.js
Normal file
10
assets/public/sound.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import Model from './model';
|
||||||
|
|
||||||
|
|
||||||
|
export default class Sound extends Model {
|
||||||
|
get name() { return this.data.name }
|
||||||
|
|
||||||
|
static getId(data) { return data.pk }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
@charset "utf-8";
|
@charset "utf-8";
|
||||||
@import "~bulma/sass/utilities/_all.sass";
|
@import "~bulma/sass/utilities/_all.sass";
|
||||||
|
@import "~bulma/sass/components/dropdown.sass";
|
||||||
|
|
||||||
$body-background-color: $light;
|
$body-background-color: $light;
|
||||||
|
|
||||||
|
@import "~buefy/src/scss/components/_autocomplete.scss";
|
||||||
@import "~bulma";
|
@import "~bulma";
|
||||||
|
|
||||||
//-- helpers/modifiers
|
//-- helpers/modifiers
|
||||||
|
@ -15,6 +17,10 @@ $body-background-color: $light;
|
||||||
}
|
}
|
||||||
.is-borderless { border: none; }
|
.is-borderless { border: none; }
|
||||||
|
|
||||||
|
.has-text-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.has-background-transparent {
|
.has-background-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -56,6 +62,22 @@ a.navbar-item.is-active {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
padding: 0em;
|
padding: 0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.toolbar {
|
||||||
|
margin: 1em 0em;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding-right: 2em;
|
||||||
|
margin-right: 1em;
|
||||||
|
border-right: 1px $grey-light solid;
|
||||||
|
|
||||||
|
font-size: $size-5;
|
||||||
|
color: $text-light;
|
||||||
|
font-weight: $weight-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-- cards
|
//-- cards
|
||||||
|
@ -91,24 +113,6 @@ a.navbar-item.is-active {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//-- filters
|
|
||||||
.filters {
|
|
||||||
margin: 1em 0em;
|
|
||||||
background-color: transparent;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding-right: 2em;
|
|
||||||
margin-right: 1em;
|
|
||||||
border-right: 1px $grey-light solid;
|
|
||||||
|
|
||||||
font-size: $size-5;
|
|
||||||
color: $text-light;
|
|
||||||
font-weight: $weight-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//-- page
|
//-- page
|
||||||
.page {
|
.page {
|
||||||
& > .cover {
|
& > .cover {
|
||||||
|
|
98
assets/streamer/controllers.js
Normal file
98
assets/streamer/controllers.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import Model from 'public/model';
|
||||||
|
|
||||||
|
|
||||||
|
export class Streamer extends Model {
|
||||||
|
get queues() { return this.data ? this.data.queues : []; }
|
||||||
|
get playlists() { return this.data ? this.data.playlists : []; }
|
||||||
|
get sources() { return [...this.queues, ...this.playlists]; }
|
||||||
|
get source() { return this.sources.find(o => o.id == this.data.source) }
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
if(!this.data)
|
||||||
|
this.data = { id: data.id, playlists: [], queues: [] }
|
||||||
|
|
||||||
|
data.playlists = Playlist.updateList(data.playlists, this.playlists)
|
||||||
|
data.queues = Queue.updateList(data.queues, this.queues)
|
||||||
|
super.commit(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Request extends Model {
|
||||||
|
static getId(data) { return data.rid; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Source extends Model {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
setInterval(() => this.tick(), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
get isQueue() { return false; }
|
||||||
|
get isPlaylist() { return false; }
|
||||||
|
get isPlaying() { return this.data.status == 'playing' }
|
||||||
|
get isPaused() { return this.data.status == 'paused' }
|
||||||
|
|
||||||
|
get remainingString() {
|
||||||
|
if(!this.remaining)
|
||||||
|
return '00:00';
|
||||||
|
|
||||||
|
const seconds = Math.floor(this.remaining % 60);
|
||||||
|
const minutes = Math.floor(this.remaining / 60);
|
||||||
|
return String(minutes).padStart(2, '0') + ':' +
|
||||||
|
String(seconds).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sync() { return this.action('sync/', {method: 'POST'}, true); }
|
||||||
|
skip() { return this.action('skip/', {method: 'POST'}, true); }
|
||||||
|
restart() { return this.action('restart/', {method: 'POST'}, true); }
|
||||||
|
|
||||||
|
seek(count) {
|
||||||
|
return this.action('seek/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({count: count})
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
if(!this.data.remaining || !this.isPlaying)
|
||||||
|
return;
|
||||||
|
const delta = (Date.now() - this.commitDate) / 1000;
|
||||||
|
Vue.set(this, 'remaining', this.data.remaining - delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
if(data.air_time)
|
||||||
|
data.air_time = new Date(data.air_time);
|
||||||
|
Vue.set(this, 'remaining', data.remaining)
|
||||||
|
this.commitDate = Date.now()
|
||||||
|
super.commit(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Playlist extends Source {
|
||||||
|
get isPlaylist() { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Queue extends Source {
|
||||||
|
get isQueue() { return true; }
|
||||||
|
get queue() { return this.data && this.data.queue; }
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
data.queue = Request.updateList(data.queue, this.queue)
|
||||||
|
super.commit(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
push(soundId) {
|
||||||
|
return this.action('push/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({'sound_id': parseInt(soundId)})
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
56
assets/streamer/index.js
Normal file
56
assets/streamer/index.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Button from 'buefy/dist/components/button';
|
||||||
|
|
||||||
|
Vue.use(Button)
|
||||||
|
|
||||||
|
import {setAppConfig} from 'public/app';
|
||||||
|
import Model from 'public/model';
|
||||||
|
import Sound from 'public/sound';
|
||||||
|
import {Streamer, Queue} from './controllers';
|
||||||
|
|
||||||
|
window.aircox.appConfig = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// current streamer
|
||||||
|
streamer: null,
|
||||||
|
// all streamers
|
||||||
|
streamers: [],
|
||||||
|
// fetch interval id
|
||||||
|
fetchInterval: null,
|
||||||
|
|
||||||
|
Sound: Sound,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
apiUrl() {
|
||||||
|
return this.$el && this.$el.dataset.apiUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
sources() {
|
||||||
|
var sources = this.streamer ? this.streamer.sources : [];
|
||||||
|
return sources.filter(s => s.data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchStreamers() {
|
||||||
|
Streamer.fetchAll(this.apiUrl, null)
|
||||||
|
.then(streamers => {
|
||||||
|
Vue.set(this, 'streamers', streamers);
|
||||||
|
Vue.set(this, 'streamer', streamers ? streamers[0] : null);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchStreamers();
|
||||||
|
this.fetchInterval = setInterval(() => this.streamer && this.streamer.fetch(), 5000)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
if(this.fetchInterval !== null)
|
||||||
|
clearInterval(this.fetchInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -121,9 +121,15 @@ except:
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
# aircox & dependencies
|
|
||||||
'aircox',
|
'aircox',
|
||||||
|
'aircox.apps.AircoxAdminConfig',
|
||||||
|
'aircox_streamer',
|
||||||
|
|
||||||
|
# aircox applications
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
|
||||||
|
# aircox_web applications
|
||||||
|
"content_editor",
|
||||||
"ckeditor",
|
"ckeditor",
|
||||||
'easy_thumbnails',
|
'easy_thumbnails',
|
||||||
'filer',
|
'filer',
|
||||||
|
|
|
@ -19,6 +19,7 @@ from django.contrib import admin
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
|
|
||||||
import aircox.urls
|
import aircox.urls
|
||||||
|
import aircox_streamer.urls
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"css-loader": "^2.1.1",
|
"css-loader": "^2.1.1",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "^3.0.1",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"sass-loader": "^7.3.1",
|
"sass-loader": "^7.3.1",
|
||||||
|
|
|
@ -11,6 +11,7 @@ module.exports = (env, argv) => Object({
|
||||||
entry: {
|
entry: {
|
||||||
main: './assets/public/index',
|
main: './assets/public/index',
|
||||||
admin: './assets/admin/index',
|
admin: './assets/admin/index',
|
||||||
|
streamer: './assets/streamer/index',
|
||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user