fix logs merge with diff algorithm

This commit is contained in:
bkfox 2019-08-12 04:11:04 +02:00
parent aabbcd97fa
commit e0f1ac498f
25 changed files with 485 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&#9836;</span>
<span>{{ track.title }}</span>
<span class="has-text-grey-dark has-text-weight-light">
&mdash; {{ 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">&#9836;</span>
<span>{{ track.title }}</span>
<span class="has-text-grey-dark has-text-weight-light">
&mdash; {{ track.artist }}
{% if track.info %}(<i>{{ track.info }}</i>){% endif %}
</span>
{% endwith %}
{% endif %}

View File

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

View File

@ -49,8 +49,6 @@
</div>
</div>
</div>
</div>
</form>
</section>
{% endblock %}

View File

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

View File

@ -1,3 +1,5 @@
from . import api
from .article import ArticleListView
from .base import BaseView
from .episode import EpisodeDetailView, EpisodeListView, TimetableView

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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