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
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
class Command(BaseCommand):
|
||||
help = __doc__
|
||||
|
||||
def add_arguments(self, parser):
|
||||
|
|
Binary file not shown.
|
@ -22,13 +22,17 @@ __all__ = ['Sound', 'SoundQuerySet', 'Track']
|
|||
|
||||
|
||||
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):
|
||||
return self.filter(episode=episode) if id is None else \
|
||||
self.filter(episode__id=id)
|
||||
id = episode.pk if id is None else id
|
||||
return self.filter(episode__id=id)
|
||||
|
||||
def diffusion(self, diffusion=None, id=None):
|
||||
return self.filter(episode__diffusion=diffusion) if id is None else \
|
||||
self.filter(episode__diffusion__id=id)
|
||||
id = diffusion.pk if id is None else id
|
||||
return self.filter(episode__diffusion__id=id)
|
||||
|
||||
def podcasts(self):
|
||||
""" Return sounds available as podcasts """
|
||||
|
@ -49,6 +53,13 @@ class SoundQuerySet(models.QuerySet):
|
|||
self = self.order_by('path')
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import Diffusion, Log
|
||||
from .models import Diffusion, Log, Sound
|
||||
|
||||
|
||||
__all__ = ['LogInfo', 'LogInfoSerializer']
|
||||
|
@ -53,3 +53,18 @@ class LogInfoSerializer(serializers.Serializer):
|
|||
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 {
|
||||
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 */
|
||||
@keyframes spinAround {
|
||||
from {
|
||||
|
@ -7177,6 +7268,9 @@ label.panel-block {
|
|||
.is-borderless {
|
||||
border: none; }
|
||||
|
||||
.has-text-nowrap {
|
||||
white-space: nowrap; }
|
||||
|
||||
.has-background-transparent {
|
||||
background-color: transparent; }
|
||||
|
||||
|
@ -7208,6 +7302,18 @@ a.navbar-item.is-active {
|
|||
margin: 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 {
|
||||
padding: 0.2em;
|
||||
font-size: 1.25rem;
|
||||
|
@ -7230,18 +7336,6 @@ a.navbar-item.is-active {
|
|||
padding: 0.1em;
|
||||
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 {
|
||||
float: right;
|
||||
max-width: 45%; }
|
||||
|
|
|
@ -218,11 +218,47 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _nod
|
|||
/*!******************************!*\
|
||||
!*** ./assets/public/app.js ***!
|
||||
\******************************/
|
||||
/*! exports provided: app, default */
|
||||
/*! exports provided: appBaseConfig, setAppConfig, getAppConfig, loadApp */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"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__) {
|
||||
|
||||
"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??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??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 {
|
||||
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 */
|
||||
@keyframes spinAround {
|
||||
from {
|
||||
|
@ -7159,6 +7250,9 @@ label.panel-block {
|
|||
.is-borderless {
|
||||
border: none; }
|
||||
|
||||
.has-text-nowrap {
|
||||
white-space: nowrap; }
|
||||
|
||||
.has-background-transparent {
|
||||
background-color: transparent; }
|
||||
|
||||
|
@ -7190,6 +7284,18 @@ a.navbar-item.is-active {
|
|||
margin: 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 {
|
||||
padding: 0.2em;
|
||||
font-size: 1.25rem;
|
||||
|
@ -7212,18 +7318,6 @@ a.navbar-item.is-active {
|
|||
padding: 0.1em;
|
||||
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 {
|
||||
float: right;
|
||||
max-width: 45%; }
|
||||
|
|
|
@ -159,11 +159,47 @@
|
|||
/*!******************************!*\
|
||||
!*** ./assets/public/app.js ***!
|
||||
\******************************/
|
||||
/*! exports provided: app, default */
|
||||
/*! exports provided: appBaseConfig, setAppConfig, getAppConfig, loadApp */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"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__) {
|
||||
|
||||
"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??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??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 %}
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||
<head>
|
||||
<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 "aircox/main.css" %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}">
|
||||
|
@ -67,7 +68,10 @@
|
|||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a href="#" class="navbar-link">{% trans "Tools" %}</a>
|
||||
<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>
|
||||
|
||||
|
@ -121,7 +125,7 @@
|
|||
<!-- Content -->
|
||||
<div id="content" class="{% block coltype %}colM{% 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 object-tools %}{% endblock %}
|
||||
{{ content }}
|
||||
|
|
|
@ -93,7 +93,7 @@ Blocks:
|
|||
|
||||
{% block main %}
|
||||
{% if has_filters %}
|
||||
<nav class="navbar filters"
|
||||
<nav class="navbar toolbar"
|
||||
aria-label="{% trans "list filters" %}">
|
||||
{% block filters %}{% endblock %}
|
||||
</nav>
|
||||
|
@ -132,8 +132,8 @@ Blocks:
|
|||
</div>
|
||||
|
||||
<hr>
|
||||
{% include "aircox/player.html" %}
|
||||
</div>
|
||||
<div id="player">{% include "aircox/player.html" %}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -38,7 +38,9 @@ Context:
|
|||
|
||||
<hr>
|
||||
<h4 class="title is-4">{% trans "Last publications" %}</h4>
|
||||
{% with has_headline=True %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</noscript>
|
||||
|
||||
<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" %}">
|
||||
<template v-slot:sources>
|
||||
{% for stream in audio_streams %}
|
||||
|
|
|
@ -12,12 +12,17 @@ __all__ = ['BaseAdminView', 'StatisticsView']
|
|||
class BaseAdminView(LoginRequiredMixin, UserPassesTestMixin):
|
||||
title = ''
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
return self.request.station
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update(admin.site.each_context(self.request))
|
||||
kwargs.setdefault('title', self.title)
|
||||
kwargs.setdefault('station', self.station)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -3,3 +3,5 @@ from django.apps import AppConfig
|
|||
|
||||
class AircoxStreamerConfig(AppConfig):
|
||||
name = 'aircox_streamer'
|
||||
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ class BaseMetadata:
|
|||
""" Request uri """
|
||||
status = None
|
||||
""" Current playing status """
|
||||
request_status = None
|
||||
""" Requests' status """
|
||||
air_time = None
|
||||
""" Launch datetime """
|
||||
|
||||
|
@ -58,10 +60,25 @@ class BaseMetadata:
|
|||
return self.status == 'playing'
|
||||
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
Validate provided data and set as attribute (must already be
|
||||
|
@ -72,12 +89,9 @@ class BaseMetadata:
|
|||
setattr(self, key, value)
|
||||
self.uri = data.get('initial_uri')
|
||||
|
||||
air_time = data.get('on_air')
|
||||
if air_time:
|
||||
air_time = tz.datetime.strptime(air_time, '%Y/%m/%d %H:%M:%S')
|
||||
self.air_time = local_tz.localize(air_time)
|
||||
else:
|
||||
self.air_time = None
|
||||
self.air_time = self.validate_air_time(data.get('on_air'))
|
||||
self.status = self.validate_status(data.get('status'))
|
||||
self.request_status = data.get('status')
|
||||
|
||||
|
||||
class Request(BaseMetadata):
|
||||
|
@ -142,6 +156,14 @@ class Streamer:
|
|||
logger.debug('process died with return code %s' % returncode)
|
||||
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 ###############################################
|
||||
def send(self, *args, **kwargs):
|
||||
return self.connector.send(*args, **kwargs) or ''
|
||||
|
@ -180,12 +202,11 @@ class Streamer:
|
|||
source.fetch()
|
||||
|
||||
# request.on_air is not ordered: we need to do it manually
|
||||
if self.dealer.is_playing:
|
||||
self.source = self.dealer
|
||||
return
|
||||
|
||||
self.source = next((source for source in self.sources
|
||||
if source.is_playing), None)
|
||||
self.source = next(iter(sorted(
|
||||
(source for source in self.sources
|
||||
if source.request_status == 'playing' and source.air_time),
|
||||
key=lambda o: o.air_time, reverse=True
|
||||
)), None)
|
||||
|
||||
# Process ##########################################################
|
||||
def get_process_args(self):
|
||||
|
@ -241,15 +262,12 @@ class Source(BaseMetadata):
|
|||
""" source id """
|
||||
remaining = 0.0
|
||||
""" remaining time """
|
||||
status = 'stopped'
|
||||
|
||||
@property
|
||||
def station(self):
|
||||
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):
|
||||
super().__init__(controller, *args, **kwargs)
|
||||
self.id = id
|
||||
|
@ -258,9 +276,12 @@ class Source(BaseMetadata):
|
|||
""" Synchronize what should be synchronized """
|
||||
|
||||
def fetch(self):
|
||||
data = self.controller.send(self.id, '.remaining')
|
||||
if data:
|
||||
self.remaining = float(data)
|
||||
try:
|
||||
data = self.controller.send(self.id, '.remaining')
|
||||
if data:
|
||||
self.remaining = float(data)
|
||||
except ValueError:
|
||||
self.remaining = None
|
||||
|
||||
data = self.controller.send(self.id, '.get', parse=True)
|
||||
if data:
|
||||
|
@ -332,12 +353,9 @@ class PlaylistSource(Source):
|
|||
class QueueSource(Source):
|
||||
queue = None
|
||||
""" 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)
|
||||
self.queue_metadata = queue_metadata
|
||||
|
||||
def push(self, *paths):
|
||||
""" Add the provided paths to source's play queue """
|
||||
|
@ -346,13 +364,19 @@ class QueueSource(Source):
|
|||
|
||||
def fetch(self):
|
||||
super().fetch()
|
||||
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
||||
if not self.as_requests:
|
||||
self.queue = queue
|
||||
queue = self.controller.send(self.id, '_queue.queue').strip()
|
||||
if not queue:
|
||||
self.queue = []
|
||||
return
|
||||
|
||||
self.queue = [Request(self.controller, rid) for rid in queue]
|
||||
for request in self.queue:
|
||||
self.queue = queue.split(' ')
|
||||
|
||||
@property
|
||||
def requests(self):
|
||||
""" Queue as requests metadata """
|
||||
requests = [Request(self.controller, rid) for rid in self.queue]
|
||||
for request in requests:
|
||||
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.utils import date_range
|
||||
|
||||
from aircox_streamer.liquidsoap import Streamer, PlaylistSource
|
||||
from aircox_streamer.controllers import Streamer
|
||||
|
||||
|
||||
# force using UTC
|
||||
|
@ -246,9 +246,8 @@ class Monitor:
|
|||
|
||||
self.sync_next = now + tz.timedelta(minutes=self.sync_timeout)
|
||||
|
||||
for source in self.streamer.sources:
|
||||
if isinstance(source, PlaylistSource):
|
||||
source.sync()
|
||||
for source in self.streamer.playlists:
|
||||
source.sync()
|
||||
|
||||
|
||||
class Command (BaseCommand):
|
||||
|
@ -291,10 +290,8 @@ class Command (BaseCommand):
|
|||
)
|
||||
# TODO: sync-timeout, cancel-timeout
|
||||
|
||||
def handle(self, *args,
|
||||
config=None, run=None, monitor=None,
|
||||
station=[], delay=1000, timeout=600,
|
||||
**options):
|
||||
def handle(self, *args, config=None, run=None, monitor=None, station=[],
|
||||
delay=1000, timeout=600, **options):
|
||||
stations = Station.objects.filter(name__in=station) if station else \
|
||||
Station.objects.all()
|
||||
streamers = [Streamer(station) for station in stations]
|
||||
|
|
|
@ -1,24 +1,35 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .controllers import QueueSource, PlaylistSource
|
||||
|
||||
|
||||
__all__ = ['RequestSerializer', 'StreamerSerializer', 'SourceSerializer',
|
||||
'PlaylistSerializer', 'QueueSourceSerializer']
|
||||
# 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()
|
||||
air_time = serializers.DateTimeField()
|
||||
uri = serializers.CharField()
|
||||
|
||||
|
||||
class RequestSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
artist = serializers.CharField()
|
||||
|
||||
|
||||
class StreamerSerializer(serializers.Serializer):
|
||||
station = serializers.CharField(source='station.title')
|
||||
class RequestSerializer(BaseMetadataSerializer):
|
||||
title = serializers.CharField(required=False)
|
||||
artist = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class SourceSerializer(BaseMetadataSerializer):
|
||||
|
@ -27,14 +38,34 @@ class SourceSerializer(BaseMetadataSerializer):
|
|||
rid = serializers.IntegerField()
|
||||
air_time = serializers.DateTimeField()
|
||||
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):
|
||||
program = serializers.CharField(source='program.title')
|
||||
playlist = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
url_name = 'admin:api:streamer-playlist-detail'
|
||||
|
||||
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.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
|
||||
from aircox import controllers
|
||||
from aircox.models import Station
|
||||
from aircox.models import Sound, Station
|
||||
from aircox.serializers import SoundSerializer
|
||||
from . import controllers
|
||||
from .serializers import *
|
||||
|
||||
|
||||
|
@ -52,16 +56,17 @@ class Streamers:
|
|||
self.date = now + self.timeout
|
||||
|
||||
def get(self, key, default=None):
|
||||
self.fetch()
|
||||
return self.streamers.get(key, default)
|
||||
|
||||
def values(self):
|
||||
self.fetch()
|
||||
return self.streamers.values()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.streamers[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.streamers
|
||||
|
||||
|
||||
streamers = Streamers()
|
||||
|
||||
|
@ -70,22 +75,25 @@ class BaseControllerAPIView(viewsets.ViewSet):
|
|||
permission_classes = (IsAdminUser,)
|
||||
serializer = None
|
||||
streamer = None
|
||||
object = None
|
||||
|
||||
def get_streamer(self, pk=None):
|
||||
streamer = streamers.get(self.request.pk if pk is None else pk)
|
||||
if not streamer:
|
||||
def get_streamer(self, request, station_pk=None, **kwargs):
|
||||
streamers.fetch()
|
||||
id = int(request.station.pk if station_pk is None else station_pk)
|
||||
if id not in streamers:
|
||||
raise Http404('station not found')
|
||||
return streamer
|
||||
return streamers[id]
|
||||
|
||||
def get_serializer(self, obj, **kwargs):
|
||||
return self.serializer(obj, **kwargs)
|
||||
def get_serializer(self, **kwargs):
|
||||
return self.serializer(self.object, **kwargs)
|
||||
|
||||
def serialize(self, obj, **kwargs):
|
||||
serializer = self.get_serializer(obj, **kwargs)
|
||||
self.object = obj
|
||||
serializer = self.get_serializer(**kwargs)
|
||||
return serializer.data
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.streamer = self.get_streamer(request.station.pk)
|
||||
def dispatch(self, request, *args, station_pk=None, **kwargs):
|
||||
self.streamer = self.get_streamer(request, station_pk, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
@ -97,10 +105,19 @@ class StreamerViewSet(BaseControllerAPIView):
|
|||
serializer = StreamerSerializer
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
return self.serialize(self.streamer)
|
||||
return Response(self.serialize(self.streamer))
|
||||
|
||||
def list(self, request):
|
||||
return self.serialize(streamers.values(), many=True)
|
||||
def list(self, request, pk=None):
|
||||
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):
|
||||
|
@ -108,38 +125,46 @@ class SourceViewSet(BaseControllerAPIView):
|
|||
model = controllers.Source
|
||||
|
||||
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):
|
||||
source = next((source for source in self.get_sources()
|
||||
if source.pk == pk), None)
|
||||
if source.id == pk), None)
|
||||
if source is None:
|
||||
raise Http404('source `%s` not found' % pk)
|
||||
return source
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
source = self.get_source(pk)
|
||||
return self.serialize(source)
|
||||
self.object = self.get_source(pk)
|
||||
return Response(self.serialize())
|
||||
|
||||
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'])
|
||||
def sync(self, request, pk):
|
||||
self.get_source(pk).sync()
|
||||
return self._run(pk, lambda s: s.sync())
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def skip(self, request, pk):
|
||||
self.get_source(pk).skip()
|
||||
return self._run(pk, lambda s: s.skip())
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def restart(self, request, pk):
|
||||
self.get_source(pk).restart()
|
||||
return self._run(pk, lambda s: s.restart())
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def seek(self, request, pk):
|
||||
count = request.POST['seek']
|
||||
self.get_source(pk).seek(count)
|
||||
return self._run(pk, lambda s: s.seek(count))
|
||||
|
||||
|
||||
class PlaylistSourceViewSet(SourceViewSet):
|
||||
|
@ -151,8 +176,26 @@ class QueueSourceViewSet(SourceViewSet):
|
|||
serializer = QueueSourceSerializer
|
||||
model = controllers.QueueSource
|
||||
|
||||
def get_sound_queryset(self):
|
||||
return Sound.objects.station(self.request.station).archive()
|
||||
|
||||
@action(detail=False, url_path='autocomplete/push',
|
||||
url_name='autocomplete-push')
|
||||
def autcomplete_push(self, request):
|
||||
query = request.GET.get('q')
|
||||
qs = self.get_sound_queryset().search(query)
|
||||
serializer = SoundSerializer(qs, many=True, context={
|
||||
'request': self.request
|
||||
})
|
||||
return Response({'results': serializer.data})
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def push(self, request, pk):
|
||||
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';
|
||||
|
||||
|
||||
export var app = null;
|
||||
export default app;
|
||||
export const appBaseConfig = {
|
||||
el: '#app',
|
||||
delimiters: ['[[', ']]'],
|
||||
}
|
||||
|
||||
function loadApp() {
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
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
|
||||
import app from './app';
|
||||
import LiveInfo from './liveInfo';
|
||||
import {appConfig, loadApp} from './app';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
import Player from './player.vue';
|
||||
import Autocomplete from './autocomplete.vue';
|
||||
|
||||
Vue.component('a-player', Player)
|
||||
Vue.component('a-autocomplete', Autocomplete)
|
||||
|
||||
|
||||
window.aircox = {
|
||||
app: app,
|
||||
LiveInfo: LiveInfo,
|
||||
}
|
||||
// main application
|
||||
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";
|
||||
@import "~bulma/sass/utilities/_all.sass";
|
||||
@import "~bulma/sass/components/dropdown.sass";
|
||||
|
||||
$body-background-color: $light;
|
||||
|
||||
@import "~buefy/src/scss/components/_autocomplete.scss";
|
||||
@import "~bulma";
|
||||
|
||||
//-- helpers/modifiers
|
||||
|
@ -15,6 +17,10 @@ $body-background-color: $light;
|
|||
}
|
||||
.is-borderless { border: none; }
|
||||
|
||||
.has-text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.has-background-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
@ -56,6 +62,22 @@ a.navbar-item.is-active {
|
|||
margin: 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
|
||||
|
@ -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 {
|
||||
& > .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
|
||||
INSTALLED_APPS = (
|
||||
# aircox & dependencies
|
||||
'aircox',
|
||||
'aircox.apps.AircoxAdminConfig',
|
||||
'aircox_streamer',
|
||||
|
||||
# aircox applications
|
||||
'rest_framework',
|
||||
|
||||
# aircox_web applications
|
||||
"content_editor",
|
||||
"ckeditor",
|
||||
'easy_thumbnails',
|
||||
'filer',
|
||||
|
|
|
@ -19,6 +19,7 @@ from django.contrib import admin
|
|||
from django.urls import include, path, re_path
|
||||
|
||||
import aircox.urls
|
||||
import aircox_streamer.urls
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"css-loader": "^2.1.1",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = (env, argv) => Object({
|
|||
entry: {
|
||||
main: './assets/public/index',
|
||||
admin: './assets/admin/index',
|
||||
streamer: './assets/streamer/index',
|
||||
},
|
||||
|
||||
output: {
|
||||
|
|
Loading…
Reference in New Issue
Block a user