forked from rc/aircox
		
	fix logs merge with diff algorithm
This commit is contained in:
		
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -1,3 +1,4 @@
 | 
			
		||||
from collections import deque
 | 
			
		||||
import datetime
 | 
			
		||||
from enum import IntEnum
 | 
			
		||||
import logging
 | 
			
		||||
@ -25,10 +26,14 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
        return self.filter(station=station) if id is None else \
 | 
			
		||||
               self.filter(station_id=id)
 | 
			
		||||
 | 
			
		||||
    def at(self, date=None):
 | 
			
		||||
        date = utils.date_or_default(date)
 | 
			
		||||
    def today(self, date):
 | 
			
		||||
        return self.filter(date__date=date)
 | 
			
		||||
 | 
			
		||||
    def after(self, date):
 | 
			
		||||
        return self.filter(date__gte=date) \
 | 
			
		||||
            if isinstance(date, tz.datetime) else \
 | 
			
		||||
            self.filter(date__date__gte=date)
 | 
			
		||||
 | 
			
		||||
    def on_air(self):
 | 
			
		||||
        return self.filter(type=Log.Type.on_air)
 | 
			
		||||
 | 
			
		||||
@ -100,13 +105,9 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            def rel_obj(log, attr):
 | 
			
		||||
                attr_id = attr + '_id'
 | 
			
		||||
                rel_id = log.get(attr + '_id')
 | 
			
		||||
 | 
			
		||||
                return rels[attr][rel_id] if rel_id else None
 | 
			
		||||
 | 
			
		||||
            # make logs
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                Log(diffusion=rel_obj(log, 'diffusion'),
 | 
			
		||||
                    sound=rel_obj(log, 'sound'),
 | 
			
		||||
@ -134,7 +135,7 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
        if os.path.exists(path) and not force:
 | 
			
		||||
            return -1
 | 
			
		||||
 | 
			
		||||
        qs = self.station(station).at(date)
 | 
			
		||||
        qs = self.station(station).today(date)
 | 
			
		||||
 | 
			
		||||
        if not qs.exists():
 | 
			
		||||
            return 0
 | 
			
		||||
@ -241,6 +242,56 @@ class Log(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        return tz.localtime(self.date, tz.get_current_timezone())
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '#{} ({}, {}, {})'.format(
 | 
			
		||||
            self.pk, self.get_type_display(),
 | 
			
		||||
            self.source, self.local_date.strftime('%Y/%m/%d %H:%M%z'))
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def __list_append(cls, object_list, items):
 | 
			
		||||
        object_list += [cls(obj) for obj in items]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def merge_diffusions(cls, logs, diffs):
 | 
			
		||||
        diffs = deque(diffs.order_by('start'))
 | 
			
		||||
        logs = list(logs.order_by('date'))
 | 
			
		||||
        object_list = []
 | 
			
		||||
 | 
			
		||||
        # +++ +++ ++ ++
 | 
			
		||||
        #  ----    ----- ----
 | 
			
		||||
        while True:
 | 
			
		||||
            if not len(diffs):
 | 
			
		||||
                object_list += logs
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            if not len(logs):
 | 
			
		||||
                object_list += diffs
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            diff = diffs.popleft()
 | 
			
		||||
 | 
			
		||||
            # - takes all logs before diff happens
 | 
			
		||||
            index = next((i for i, v in enumerate(logs)
 | 
			
		||||
                          if v.date >= diff.start), len(logs))
 | 
			
		||||
            if index is not None and index > 0:
 | 
			
		||||
                object_list += logs[:index]
 | 
			
		||||
                logs = logs[index:]
 | 
			
		||||
 | 
			
		||||
            # - add diff
 | 
			
		||||
            object_list.append(diff)
 | 
			
		||||
 | 
			
		||||
            # - last log while diff is running
 | 
			
		||||
            # Using of greater allow last_log to log that starts with date
 | 
			
		||||
            # equals to diff.end
 | 
			
		||||
            index = next((i for i, v in enumerate(logs) if v.date > diff.end),
 | 
			
		||||
                         None)
 | 
			
		||||
            if index is not None and index > 0:
 | 
			
		||||
                object_list.append(logs[index-1])
 | 
			
		||||
                logs = logs[index:]
 | 
			
		||||
 | 
			
		||||
        return object_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def print(self):
 | 
			
		||||
        r = []
 | 
			
		||||
        if self.diffusion:
 | 
			
		||||
@ -252,7 +303,3 @@ class Log(models.Model):
 | 
			
		||||
        logger.info('log %s: %s%s', str(self), self.comment or '',
 | 
			
		||||
                    ' (' + ', '.join(r) + ')' if r else '')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '#{} ({}, {}, {})'.format(
 | 
			
		||||
            self.pk, self.get_type_display(),
 | 
			
		||||
            self.source, self.local_date.strftime('%Y/%m/%d %H:%M%z'))
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ def program_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
    Clean-up later diffusions when a program becomes inactive
 | 
			
		||||
    """
 | 
			
		||||
    if not instance.active:
 | 
			
		||||
        Diffusion.objects.program(instance).after().delete()
 | 
			
		||||
        Diffusion.objects.program(instance).after(tz.now()).delete()
 | 
			
		||||
        Episode.object.program(instance).filter(diffusion__isnull=True) \
 | 
			
		||||
               .delete()
 | 
			
		||||
 | 
			
		||||
@ -70,7 +70,7 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
    today = tz.datetime.today()
 | 
			
		||||
    delta = instance.normalize(today) - initial.normalize(today)
 | 
			
		||||
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after()
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after(tz.now())
 | 
			
		||||
    pks = [d.pk for d in qs if initial.match(d.date)]
 | 
			
		||||
    qs.filter(pk__in=pks).update(
 | 
			
		||||
        start=F('start') + delta,
 | 
			
		||||
@ -86,7 +86,7 @@ def schedule_pre_delete(sender, instance, *args, **kwargs):
 | 
			
		||||
    if not instance.program.sync:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after()
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after(tz.now())
 | 
			
		||||
    pks = [d.pk for d in qs if instance.match(d.date)]
 | 
			
		||||
    qs.filter(pk__in=pks).delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,11 @@ class Station(models.Model):
 | 
			
		||||
        _("website's urls"), max_length=512, null=True, blank=True,
 | 
			
		||||
        help_text=_('specify one url per line')
 | 
			
		||||
    )
 | 
			
		||||
    audio_streams = models.TextField(
 | 
			
		||||
        _("audio streams"), max_length=2048, null=True, blank=True,
 | 
			
		||||
        help_text=_("Audio streams urls used by station's player. One url "
 | 
			
		||||
                    "a line.")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = StationQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7147,15 +7147,25 @@ label.panel-block {
 | 
			
		||||
  background-color: #fafafa;
 | 
			
		||||
  padding: 3rem 1.5rem 6rem; }
 | 
			
		||||
 | 
			
		||||
.navbar {
 | 
			
		||||
  margin-bottom: 1em; }
 | 
			
		||||
.is-fullwidth {
 | 
			
		||||
  width: 100%; }
 | 
			
		||||
 | 
			
		||||
.navbar.has-shadow {
 | 
			
		||||
  box-shadow: 0em 0.05em 0.5em rgba(0, 0, 0, 0.1); }
 | 
			
		||||
.is-fixed-bottom {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  margin-bottom: 0px;
 | 
			
		||||
  border-radius: 0; }
 | 
			
		||||
 | 
			
		||||
.is-borderless {
 | 
			
		||||
  border: none; }
 | 
			
		||||
 | 
			
		||||
.navbar + .container {
 | 
			
		||||
  margin-top: 1em; }
 | 
			
		||||
 | 
			
		||||
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); }
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.navbar-brand img {
 | 
			
		||||
    min-height: 6em;
 | 
			
		||||
}
 | 
			
		||||
@ -7200,7 +7210,3 @@ aside .media .subtitle {
 | 
			
		||||
aside .media .content {
 | 
			
		||||
  display: none; }
 | 
			
		||||
 | 
			
		||||
/**[noscript="hidden"] {
 | 
			
		||||
    display: none;
 | 
			
		||||
}*/
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -164,7 +164,7 @@
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./js */ \"./assets/js/index.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./styles.scss */ \"./assets/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _noscript_scss__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./noscript.scss */ \"./assets/noscript.scss\");\n/* harmony import */ var _noscript_scss__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_noscript_scss__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./vue */ \"./assets/vue/index.js\");\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/index.js?");
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __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_0___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_1__ = __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_1___default = /*#__PURE__*/__webpack_require__.n(_fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./js */ \"./assets/js/index.js\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./styles.scss */ \"./assets/styles.scss\");\n/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_styles_scss__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./vue */ \"./assets/vue/index.js\");\n\n\n\n\n\n// import './noscript.scss';\n\n\n\n\n//# sourceURL=webpack:///./assets/index.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
@ -176,18 +176,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _js_
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy */ \"./node_modules/buefy/dist/buefy.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(buefy__WEBPACK_IMPORTED_MODULE_1___default.a);\n\nwindow.addEventListener('load', () => {\n    var app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]({\n      el: '#app',\n      delimiters: [ '[[', ']]' ],\n    })\n});\n\n\n\n\n\n//# sourceURL=webpack:///./assets/js/index.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/noscript.scss":
 | 
			
		||||
/*!******************************!*\
 | 
			
		||||
  !*** ./assets/noscript.scss ***!
 | 
			
		||||
  \******************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/***/ (function(module, exports, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./assets/noscript.scss?");
 | 
			
		||||
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! buefy */ \"./node_modules/buefy/dist/buefy.js\");\n/* harmony import */ var buefy__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(buefy__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(buefy__WEBPACK_IMPORTED_MODULE_1___default.a);\n\nvar app = null;\n\nfunction loadApp() {\n    app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]({\n      el: '#app',\n      delimiters: [ '[[', ']]' ],\n    })\n}\n\n\nwindow.addEventListener('load', loadApp);\n\n\n\n\n//# sourceURL=webpack:///./assets/js/index.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
@ -206,11 +195,89 @@ eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./asse
 | 
			
		||||
/*!*****************************!*\
 | 
			
		||||
  !*** ./assets/vue/index.js ***!
 | 
			
		||||
  \*****************************/
 | 
			
		||||
/*! exports provided: Tab, Tabs */
 | 
			
		||||
/*! exports provided: Player, Tab, Tabs */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _tab_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./tab.vue */ \"./assets/vue/tab.vue\");\n/* harmony import */ var _tabs_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./tabs.vue */ \"./assets/vue/tabs.vue\");\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tab', _tab_vue__WEBPACK_IMPORTED_MODULE_1__[/* default */ \"a\"]);\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tabs', _tabs_vue__WEBPACK_IMPORTED_MODULE_2__[/* default */ \"a\"]);\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/vue/index.js?");
 | 
			
		||||
eval("/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n/* harmony import */ var _onAir_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./onAir.vue */ \"./assets/vue/onAir.vue\");\n/* harmony import */ var _player_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./player.vue */ \"./assets/vue/player.vue\");\n/* harmony import */ var _tab_vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./tab.vue */ \"./assets/vue/tab.vue\");\n/* harmony import */ var _tabs_vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./tabs.vue */ \"./assets/vue/tabs.vue\");\n\n\n\n\n\n\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-on-air', _onAir_vue__WEBPACK_IMPORTED_MODULE_5__[\"default\"]);\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-player', _player_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"]);\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tab', _tab_vue__WEBPACK_IMPORTED_MODULE_3__[/* default */ \"a\"]);\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].component('a-tabs', _tabs_vue__WEBPACK_IMPORTED_MODULE_4__[/* default */ \"a\"]);\n\n\n\n\n\n\n//# sourceURL=webpack:///./assets/vue/index.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/onAir.vue":
 | 
			
		||||
/*!******************************!*\
 | 
			
		||||
  !*** ./assets/vue/onAir.vue ***!
 | 
			
		||||
  \******************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _onAir_vue_vue_type_template_id_0971e224___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./onAir.vue?vue&type=template&id=0971e224& */ \"./assets/vue/onAir.vue?vue&type=template&id=0971e224&\");\n/* harmony import */ var _onAir_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./onAir.vue?vue&type=script&lang=js& */ \"./assets/vue/onAir.vue?vue&type=script&lang=js&\");\n/* 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 */ \"a\"])(\n  _onAir_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n  _onAir_vue_vue_type_template_id_0971e224___WEBPACK_IMPORTED_MODULE_0__[/* render */ \"a\"],\n  _onAir_vue_vue_type_template_id_0971e224___WEBPACK_IMPORTED_MODULE_0__[/* staticRenderFns */ \"b\"],\n  false,\n  null,\n  null,\n  null\n  \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/vue/onAir.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/vue/onAir.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/onAir.vue?vue&type=script&lang=js&":
 | 
			
		||||
/*!*******************************************************!*\
 | 
			
		||||
  !*** ./assets/vue/onAir.vue?vue&type=script&lang=js& ***!
 | 
			
		||||
  \*******************************************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_onAir_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./onAir.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/vue/onAir.vue?vue&type=script&lang=js&\");\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_onAir_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[/* default */ \"a\"]); \n\n//# sourceURL=webpack:///./assets/vue/onAir.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/onAir.vue?vue&type=template&id=0971e224&":
 | 
			
		||||
/*!*************************************************************!*\
 | 
			
		||||
  !*** ./assets/vue/onAir.vue?vue&type=template&id=0971e224& ***!
 | 
			
		||||
  \*************************************************************/
 | 
			
		||||
/*! exports provided: render, staticRenderFns */
 | 
			
		||||
/*! exports used: render, staticRenderFns */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_onAir_vue_vue_type_template_id_0971e224___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!./onAir.vue?vue&type=template&id=0971e224& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/onAir.vue?vue&type=template&id=0971e224&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_onAir_vue_vue_type_template_id_0971e224___WEBPACK_IMPORTED_MODULE_0__[\"a\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_onAir_vue_vue_type_template_id_0971e224___WEBPACK_IMPORTED_MODULE_0__[\"b\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/vue/onAir.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/player.vue":
 | 
			
		||||
/*!*******************************!*\
 | 
			
		||||
  !*** ./assets/vue/player.vue ***!
 | 
			
		||||
  \*******************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _player_vue_vue_type_template_id_2882cef8___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./player.vue?vue&type=template&id=2882cef8& */ \"./assets/vue/player.vue?vue&type=template&id=2882cef8&\");\n/* harmony import */ var _player_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./player.vue?vue&type=script&lang=js& */ \"./assets/vue/player.vue?vue&type=script&lang=js&\");\n/* 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 */ \"a\"])(\n  _player_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n  _player_vue_vue_type_template_id_2882cef8___WEBPACK_IMPORTED_MODULE_0__[/* render */ \"a\"],\n  _player_vue_vue_type_template_id_2882cef8___WEBPACK_IMPORTED_MODULE_0__[/* staticRenderFns */ \"b\"],\n  false,\n  null,\n  null,\n  null\n  \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"assets/vue/player.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./assets/vue/player.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/player.vue?vue&type=script&lang=js&":
 | 
			
		||||
/*!********************************************************!*\
 | 
			
		||||
  !*** ./assets/vue/player.vue?vue&type=script&lang=js& ***!
 | 
			
		||||
  \********************************************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _node_modules_vue_loader_lib_index_js_vue_loader_options_player_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/vue-loader/lib??vue-loader-options!./player.vue?vue&type=script&lang=js& */ \"./node_modules/vue-loader/lib/index.js?!./assets/vue/player.vue?vue&type=script&lang=js&\");\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_vue_loader_lib_index_js_vue_loader_options_player_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[/* default */ \"a\"]); \n\n//# sourceURL=webpack:///./assets/vue/player.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./assets/vue/player.vue?vue&type=template&id=2882cef8&":
 | 
			
		||||
/*!**************************************************************!*\
 | 
			
		||||
  !*** ./assets/vue/player.vue?vue&type=template&id=2882cef8& ***!
 | 
			
		||||
  \**************************************************************/
 | 
			
		||||
/*! exports provided: render, staticRenderFns */
 | 
			
		||||
/*! exports used: render, staticRenderFns */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_player_vue_vue_type_template_id_2882cef8___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!./player.vue?vue&type=template&id=2882cef8& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/player.vue?vue&type=template&id=2882cef8&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_player_vue_vue_type_template_id_2882cef8___WEBPACK_IMPORTED_MODULE_0__[\"a\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_player_vue_vue_type_template_id_2882cef8___WEBPACK_IMPORTED_MODULE_0__[\"b\"]; });\n\n\n\n//# sourceURL=webpack:///./assets/vue/player.vue?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
@ -292,6 +359,32 @@ eval("/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoad
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/vue/onAir.vue?vue&type=script&lang=js&":
 | 
			
		||||
/*!*********************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/onAir.vue?vue&type=script&lang=js& ***!
 | 
			
		||||
  \*********************************************************************************************************/
 | 
			
		||||
/*! exports provided: default */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("//\n//\n//\n//\n//\n//\n//\n\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n    props: {\n        url: String,\n        timeout: Number,\n    },\n\n    data() {\n        return {\n            // promise is set to null on destroy: this is used as check\n            // for timeout to know wether to repeat itself or not.\n            promise: null,\n            // on air infos\n            on_air: null\n        }\n    },\n\n\n    methods: {\n        fetch() {\n            const promise = fetch(url).then(response =>\n                reponse.ok ? response.json\n                           : Promise.reject(response)\n            ).then(data => {\n                this.on_air = data.results;\n                return this.on_air\n            })\n\n            this.promise = promise;\n            return promise;\n        },\n\n        refresh() {\n            const promise = this.fetch();\n            promise.then(data => {\n                if(promise != this.promise)\n                    return;\n\n                window.setTimeout(() => this.update(), this.timeout*1000)\n            })\n        },\n    },\n\n    mounted() {\n        this.update()\n    },\n\n    destroyed() {\n        this.promise = null;\n    }\n\n});\n\n\n//# sourceURL=webpack:///./assets/vue/onAir.vue?./node_modules/vue-loader/lib??vue-loader-options");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/vue/player.vue?vue&type=script&lang=js&":
 | 
			
		||||
/*!**********************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/player.vue?vue&type=script&lang=js& ***!
 | 
			
		||||
  \**********************************************************************************************************/
 | 
			
		||||
/*! exports provided: State, default */
 | 
			
		||||
/*! exports used: default */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* unused harmony export State */\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nconst State = {\n    paused: 0,\n    playing: 1,\n    loading: 2,\n}\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n    data() {\n        return {\n            state: State.paused,\n        }\n    },\n\n    props: {\n        onAir: String,\n        src: String,\n    },\n\n    computed: {\n        paused() { return this.state == State.paused; },\n        playing() { return this.state == State.playing; },\n        loading() { return this.state == State.loading; },\n    },\n\n    methods: {\n        load(src) {\n            const audio = this.$refs.audio;\n            audio.src = src;\n            audio.load()\n        },\n\n        play(src) {\n            if(src)\n                this.load(src);\n            this.$refs.audio.play().catch(e => console.error(e))\n        },\n\n        pause() {\n            this.$refs.audio.pause()\n        },\n\n        toggle() {\n            if(this.paused)\n                this.play()\n            else\n                this.pause()\n        },\n\n        onChange(event) {\n            const audio = this.$refs.audio;\n            this.state = audio.paused ? State.paused : State.playing;\n        },\n    },\n\n    mounted() {\n        this.load(this.src);\n    }\n});\n\n\n\n//# sourceURL=webpack:///./assets/vue/player.vue?./node_modules/vue-loader/lib??vue-loader-options");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/vue-loader/lib/index.js?!./assets/vue/tab.vue?vue&type=script&lang=js&":
 | 
			
		||||
/*!*******************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tab.vue?vue&type=script&lang=js& ***!
 | 
			
		||||
@ -318,6 +411,32 @@ eval("//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n/* harmony default export */ __w
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./assets/vue/onAir.vue?vue&type=template&id=0971e224&":
 | 
			
		||||
/*!*******************************************************************************************************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/onAir.vue?vue&type=template&id=0971e224& ***!
 | 
			
		||||
  \*******************************************************************************************************************************************************************************************/
 | 
			
		||||
/*! exports provided: render, staticRenderFns */
 | 
			
		||||
/*! exports used: render, staticRenderFns */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", 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(\"div\", { staticClass: \"media\" })\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/onAir.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/vue/player.vue?vue&type=template&id=2882cef8&":
 | 
			
		||||
/*!********************************************************************************************************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/player.vue?vue&type=template&id=2882cef8& ***!
 | 
			
		||||
  \********************************************************************************************************************************************************************************************/
 | 
			
		||||
/*! exports provided: render, staticRenderFns */
 | 
			
		||||
/*! exports used: render, staticRenderFns */
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", 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(\"div\", { staticClass: \"media\" }, [\n    _c(\"div\", { staticClass: \"media-left\" }, [\n      _c(\n        \"div\",\n        {\n          staticClass: \"button is-size-4\",\n          on: {\n            click: function($event) {\n              return _vm.toggle()\n            }\n          }\n        },\n        [\n          _vm.playing\n            ? _c(\"span\", { staticClass: \"fas fa-pause\" })\n            : _c(\"span\", { staticClass: \"fas fa-play\" })\n        ]\n      ),\n      _vm._v(\" \"),\n      _c(\n        \"audio\",\n        {\n          ref: \"audio\",\n          on: {\n            playing: _vm.onChange,\n            ended: _vm.onChange,\n            pause: _vm.onChange\n          }\n        },\n        [_vm._t(\"sources\")],\n        2\n      )\n    ]),\n    _vm._v(\" \"),\n    _c(\"div\", { staticClass: \"media-content\" })\n  ])\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/player.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/vue/tab.vue?vue&type=template&id=65401e0e&":
 | 
			
		||||
/*!*****************************************************************************************************************************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./assets/vue/tab.vue?vue&type=template&id=65401e0e& ***!
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,27 @@
 | 
			
		||||
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["vendor"],{
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/@fortawesome/fontawesome-free/css/all.min.css":
 | 
			
		||||
/*!********************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/@fortawesome/fontawesome-free/css/all.min.css ***!
 | 
			
		||||
  \********************************************************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/***/ (function(module, exports, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./node_modules/@fortawesome/fontawesome-free/css/all.min.css?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css":
 | 
			
		||||
/*!****************************************************************************!*\
 | 
			
		||||
  !*** ./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css ***!
 | 
			
		||||
  \****************************************************************************/
 | 
			
		||||
/*! no static exports found */
 | 
			
		||||
/***/ (function(module, exports, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
/***/ "./node_modules/buefy/dist/buefy.js":
 | 
			
		||||
/*!******************************************!*\
 | 
			
		||||
  !*** ./node_modules/buefy/dist/buefy.js ***!
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ Context:
 | 
			
		||||
 | 
			
		||||
        {% block assets %}
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}"/>
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"/>
 | 
			
		||||
        <script src="{% static "aircox/main.js" %}"></script>
 | 
			
		||||
        <script src="{% static "aircox/vendor.js" %}"></script>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
@ -80,6 +81,9 @@ Context:
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <hr>
 | 
			
		||||
            {% include "aircox/player.html" %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,24 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
Context objects:
 | 
			
		||||
- object: object to render
 | 
			
		||||
- hide_schedule: if true, hide the schedule
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
 | 
			
		||||
{% with object.track as track %}
 | 
			
		||||
<span class="has-text-info is-size-5">♬</span>
 | 
			
		||||
<span>{{ track.title }}</span>
 | 
			
		||||
<span class="has-text-grey-dark has-text-weight-light">
 | 
			
		||||
— {{ track.artist }}
 | 
			
		||||
{% if track.info %}(<i>{{ track.info }}</i>){% endif %}
 | 
			
		||||
</span>
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% if object|is_diffusion %}
 | 
			
		||||
    {% with object as diffusion %}
 | 
			
		||||
    {% with diffusion.episode as object %}
 | 
			
		||||
    {% include "aircox/episode_item.html" %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
{% else %}
 | 
			
		||||
    {% with object.track as track %}
 | 
			
		||||
    <span class="has-text-info is-size-5">♬</span>
 | 
			
		||||
    <span>{{ track.title }}</span>
 | 
			
		||||
    <span class="has-text-grey-dark has-text-weight-light">
 | 
			
		||||
    — {{ track.artist }}
 | 
			
		||||
    {% if track.info %}(<i>{{ track.info }}</i>){% endif %}
 | 
			
		||||
    </span>
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,25 +33,18 @@
 | 
			
		||||
    <table class="table is-striped is-hoverable is-fullwidth">
 | 
			
		||||
        {% for object in object_list reversed %}
 | 
			
		||||
        <tr>
 | 
			
		||||
        {% if object|is_diffusion %}
 | 
			
		||||
            <td>
 | 
			
		||||
                {% if object|is_diffusion %}
 | 
			
		||||
                <time datetime="{{ object.start }}" title="{{ object.start }}">
 | 
			
		||||
                    {{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }}
 | 
			
		||||
                </time>
 | 
			
		||||
            </td>
 | 
			
		||||
            {% with object as diffusion %}
 | 
			
		||||
            {% with diffusion.episode as object %}
 | 
			
		||||
            <td>{% include "aircox/episode_item.html" %}</td>
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
        {% else %}
 | 
			
		||||
            <td>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <time datetime="{{ object.date }}" title="{{ object.date }}">
 | 
			
		||||
                    {{ object.date|date:"H:i" }}
 | 
			
		||||
                </time>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{% include "aircox/log_item.html" %}</td>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
@ -49,8 +49,6 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
from django.urls import path, register_converter
 | 
			
		||||
from django.urls import include, path, register_converter
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from . import views, models
 | 
			
		||||
@ -17,7 +17,13 @@ register_converter(WeekConverter, 'week')
 | 
			
		||||
# ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api = [
 | 
			
		||||
    path('on-air/', views.api.OnAirAPIView.as_view(), name='on-air'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
urls = [
 | 
			
		||||
    path('api/', include(api)),
 | 
			
		||||
    # path('', views.PageDetailView.as_view(model=models.Article),
 | 
			
		||||
    #     name='home'),
 | 
			
		||||
    path(_('articles/'),
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
from . import api
 | 
			
		||||
 | 
			
		||||
from .article import ArticleListView
 | 
			
		||||
from .base import BaseView
 | 
			
		||||
from .episode import EpisodeDetailView, EpisodeListView, TimetableView
 | 
			
		||||
 | 
			
		||||
@ -28,5 +28,10 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
			
		||||
        kwargs.setdefault('station', self.station)
 | 
			
		||||
        kwargs.setdefault('cover', self.cover)
 | 
			
		||||
        kwargs.setdefault('show_side_nav', self.show_side_nav)
 | 
			
		||||
 | 
			
		||||
        if not 'audio_streams' in kwargs:
 | 
			
		||||
            streams = self.station.audio_streams
 | 
			
		||||
            streams = streams and streams.split('\n')
 | 
			
		||||
            kwargs['audio_streams'] = streams
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,66 +7,22 @@ from ..models import Diffusion, Log
 | 
			
		||||
from .base import BaseView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['BaseLogView', 'LogListView']
 | 
			
		||||
__all__ = ['BaseLogListView', 'LogListView']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseLogView(ListView):
 | 
			
		||||
    station = None
 | 
			
		||||
class BaseLogListView:
 | 
			
		||||
    date = None
 | 
			
		||||
    delta = None
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        # only get logs for tracks: log for diffusion will be retrieved
 | 
			
		||||
        # by the diffusions' queryset.
 | 
			
		||||
        return super().get_queryset().station(self.station).on_air() \
 | 
			
		||||
                      .at(self.date).filter(track__isnull=False)
 | 
			
		||||
        return super().get_queryset().on_air().filter(track__isnull=False)
 | 
			
		||||
 | 
			
		||||
    def get_diffusions_queryset(self):
 | 
			
		||||
        return Diffusion.objects.station(self.station).on_air() \
 | 
			
		||||
                                .today(self.date)
 | 
			
		||||
 | 
			
		||||
    def get_object_list(self, queryset):
 | 
			
		||||
        diffs = deque(self.get_diffusions_queryset().order_by('start'))
 | 
			
		||||
        logs = list(queryset.order_by('date'))
 | 
			
		||||
        if not len(diffs):
 | 
			
		||||
            return logs
 | 
			
		||||
 | 
			
		||||
        object_list = []
 | 
			
		||||
        diff = None
 | 
			
		||||
        last_collision = None
 | 
			
		||||
 | 
			
		||||
        # TODO/FIXME: multiple diffs at once - recheck the whole algorithm in
 | 
			
		||||
        #       detail -- however I barely see cases except when there are diff
 | 
			
		||||
        #       collision or the streamer is not working
 | 
			
		||||
        for index, log in enumerate(logs):
 | 
			
		||||
            # get next diff
 | 
			
		||||
            if diff is None or diff.end < log.date:
 | 
			
		||||
                diff = diffs.popleft() if len(diffs) else None
 | 
			
		||||
 | 
			
		||||
            # no more diff that can collide: return list
 | 
			
		||||
            if diff is None:
 | 
			
		||||
                if last_collision and not object_list or \
 | 
			
		||||
                        object_list[-1] is not last_collision:
 | 
			
		||||
                    object_list.append(last_collision)
 | 
			
		||||
                return object_list + logs[index:]
 | 
			
		||||
 | 
			
		||||
            # diff colliding with log
 | 
			
		||||
            if diff.start <= log.date:
 | 
			
		||||
                if not object_list or object_list[-1] is not diff:
 | 
			
		||||
                    object_list.append(diff)
 | 
			
		||||
                if log.date <= diff.end:
 | 
			
		||||
                    last_collision = log
 | 
			
		||||
            else:
 | 
			
		||||
                # add last colliding log: track
 | 
			
		||||
                if last_collision is not None:
 | 
			
		||||
                    object_list.append(last_collision)
 | 
			
		||||
 | 
			
		||||
                object_list.append(log)
 | 
			
		||||
                last_collision = None
 | 
			
		||||
        return object_list
 | 
			
		||||
        return Diffusion.objects.station(self.station).on_air()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogListView(BaseView, BaseLogView):
 | 
			
		||||
class LogListView(BaseView, BaseLogListView, ListView):
 | 
			
		||||
    model = Log
 | 
			
		||||
 | 
			
		||||
    date = None
 | 
			
		||||
@ -80,6 +36,14 @@ class LogListView(BaseView, BaseLogView):
 | 
			
		||||
            if 'date' in self.kwargs else today
 | 
			
		||||
        return super().get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        # only get logs for tracks: log for diffusion will be retrieved
 | 
			
		||||
        # by the diffusions' queryset.
 | 
			
		||||
        return super().get_queryset().today(self.date)
 | 
			
		||||
 | 
			
		||||
    def get_diffusions_queryset(self):
 | 
			
		||||
        return super().get_diffusions_queryset().today(self.date)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        today = datetime.date.today()
 | 
			
		||||
        max_date = min(max(self.date + datetime.timedelta(days=3),
 | 
			
		||||
@ -91,6 +55,9 @@ class LogListView(BaseView, BaseLogView):
 | 
			
		||||
            dates=(date for date in (
 | 
			
		||||
                max_date - datetime.timedelta(days=i)
 | 
			
		||||
                for i in range(0, 7)) if date >= self.min_date),
 | 
			
		||||
            object_list=self.get_object_list(self.object_list),
 | 
			
		||||
            object_list=Log.merge_diffusions(self.object_list,
 | 
			
		||||
                                             self.get_diffusions_queryset()),
 | 
			
		||||
            **kwargs
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/all.min.css';
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
 | 
			
		||||
 | 
			
		||||
import './js';
 | 
			
		||||
import './styles.scss';
 | 
			
		||||
import './noscript.scss';
 | 
			
		||||
// import './noscript.scss';
 | 
			
		||||
import './vue';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,12 +3,16 @@ import Buefy from 'buefy';
 | 
			
		||||
 | 
			
		||||
Vue.use(Buefy);
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', () => {
 | 
			
		||||
    var app = new Vue({
 | 
			
		||||
var app = null;
 | 
			
		||||
 | 
			
		||||
function loadApp() {
 | 
			
		||||
    app = new Vue({
 | 
			
		||||
      el: '#app',
 | 
			
		||||
      delimiters: [ '[[', ']]' ],
 | 
			
		||||
    })
 | 
			
		||||
});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', loadApp);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,17 +5,25 @@ $body-background-color: $light;
 | 
			
		||||
 | 
			
		||||
@import "~bulma/bulma";
 | 
			
		||||
 | 
			
		||||
.navbar {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
.is-fullwidth { width: 100%; }
 | 
			
		||||
.is-fixed-bottom {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    margin-bottom: 0px;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
.is-borderless { border: none; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.navbar + .container {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar.has-shadow {
 | 
			
		||||
    box-shadow: 0em 0.05em 0.5em rgba(0,0,0,0.1);
 | 
			
		||||
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
 | 
			
		||||
    box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.navbar-brand img {
 | 
			
		||||
    min-height: 6em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,15 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
import OnAir from './onAir.vue';
 | 
			
		||||
import Player from './player.vue';
 | 
			
		||||
import Tab from './tab.vue';
 | 
			
		||||
import Tabs from './tabs.vue';
 | 
			
		||||
 | 
			
		||||
Vue.component('a-on-air', OnAir);
 | 
			
		||||
Vue.component('a-player', Player);
 | 
			
		||||
Vue.component('a-tab', Tab);
 | 
			
		||||
Vue.component('a-tabs', Tabs);
 | 
			
		||||
 | 
			
		||||
export {Tab, Tabs};
 | 
			
		||||
export {Player, Tab, Tabs};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								assets/vue/onAir.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								assets/vue/onAir.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="media">
 | 
			
		||||
        <!-- TODO HERE -->
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        url: String,
 | 
			
		||||
        timeout: Number,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            // promise is set to null on destroy: this is used as check
 | 
			
		||||
            // for timeout to know wether to repeat itself or not.
 | 
			
		||||
            promise: null,
 | 
			
		||||
            // on air infos
 | 
			
		||||
            on_air: null
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        fetch() {
 | 
			
		||||
            const promise = fetch(url).then(response =>
 | 
			
		||||
                reponse.ok ? response.json
 | 
			
		||||
                           : Promise.reject(response)
 | 
			
		||||
            ).then(data => {
 | 
			
		||||
                this.on_air = data.results;
 | 
			
		||||
                return this.on_air
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            this.promise = promise;
 | 
			
		||||
            return promise;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        refresh() {
 | 
			
		||||
            const promise = this.fetch();
 | 
			
		||||
            promise.then(data => {
 | 
			
		||||
                if(promise != this.promise)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                window.setTimeout(() => this.update(), this.timeout*1000)
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.update()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    destroyed() {
 | 
			
		||||
        this.promise = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										82
									
								
								assets/vue/player.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								assets/vue/player.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="media">
 | 
			
		||||
        <div class="media-left">
 | 
			
		||||
            <div class="button is-size-4" @click="toggle()">
 | 
			
		||||
                <span class="fas fa-pause" v-if="playing"></span>
 | 
			
		||||
                <span class="fas fa-play" v-else></span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <audio ref="audio" @playing="onChange" @ended="onChange" @pause="onChange">
 | 
			
		||||
                <slot name="sources"></slot>
 | 
			
		||||
            </audio>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="media-content">
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export const State = {
 | 
			
		||||
    paused: 0,
 | 
			
		||||
    playing: 1,
 | 
			
		||||
    loading: 2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            state: State.paused,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        onAir: String,
 | 
			
		||||
        src: String,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        paused() { return this.state == State.paused; },
 | 
			
		||||
        playing() { return this.state == State.playing; },
 | 
			
		||||
        loading() { return this.state == State.loading; },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        load(src) {
 | 
			
		||||
            const audio = this.$refs.audio;
 | 
			
		||||
            audio.src = src;
 | 
			
		||||
            audio.load()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        play(src) {
 | 
			
		||||
            if(src)
 | 
			
		||||
                this.load(src);
 | 
			
		||||
            this.$refs.audio.play().catch(e => console.error(e))
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        pause() {
 | 
			
		||||
            this.$refs.audio.pause()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        toggle() {
 | 
			
		||||
            if(this.paused)
 | 
			
		||||
                this.play()
 | 
			
		||||
            else
 | 
			
		||||
                this.pause()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onChange(event) {
 | 
			
		||||
            const audio = this.$refs.audio;
 | 
			
		||||
            this.state = audio.paused ? State.paused : State.playing;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.load(this.src);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								notes.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								notes.md
									
									
									
									
									
								
							@ -1,5 +1,16 @@
 | 
			
		||||
This file is used as a reminder, can be used as crappy documentation too.
 | 
			
		||||
 | 
			
		||||
- player
 | 
			
		||||
- monitor interface
 | 
			
		||||
- statistics interface
 | 
			
		||||
- traduction
 | 
			
		||||
- hot reload
 | 
			
		||||
 | 
			
		||||
Améliorations:
 | 
			
		||||
- calendar dashboard
 | 
			
		||||
- accessibilité
 | 
			
		||||
- player: playlist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# for the 1.0
 | 
			
		||||
- logs:
 | 
			
		||||
 | 
			
		||||
@ -56,13 +56,12 @@ module.exports = (env, argv) => Object({
 | 
			
		||||
                sideEffects: false
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                test: /\.scss$/,
 | 
			
		||||
                test: /\.s?css$/,
 | 
			
		||||
                use: [ { loader: MiniCssExtractPlugin.loader },
 | 
			
		||||
                       { loader: 'css-loader' },
 | 
			
		||||
                       { loader: 'sass-loader' , options: { sourceMap: true }} ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                // TODO: remove ttf eot svg
 | 
			
		||||
                test: /\.(ttf|eot|svg|woff2?)$/,
 | 
			
		||||
                use: [{
 | 
			
		||||
                    loader: 'file-loader',
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user