diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc index f5130d9..2b2f323 100644 Binary files a/aircox/models/__pycache__/__init__.cpython-37.pyc and b/aircox/models/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/log.cpython-37.pyc b/aircox/models/__pycache__/log.cpython-37.pyc index 556d5da..148a792 100644 Binary files a/aircox/models/__pycache__/log.cpython-37.pyc and b/aircox/models/__pycache__/log.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/station.cpython-37.pyc b/aircox/models/__pycache__/station.cpython-37.pyc index e23bf84..fb3904f 100644 Binary files a/aircox/models/__pycache__/station.cpython-37.pyc and b/aircox/models/__pycache__/station.cpython-37.pyc differ diff --git a/aircox/models/log.py b/aircox/models/log.py index 6caf041..ff067de 100644 --- a/aircox/models/log.py +++ b/aircox/models/log.py @@ -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')) diff --git a/aircox/models/signals.py b/aircox/models/signals.py index 7b2ad76..dfab8ef 100755 --- a/aircox/models/signals.py +++ b/aircox/models/signals.py @@ -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() diff --git a/aircox/models/station.py b/aircox/models/station.py index e18f431..751cdf1 100644 --- a/aircox/models/station.py +++ b/aircox/models/station.py @@ -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() diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index 35c8116..3c0d3ef 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -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; -}*/ - diff --git a/aircox/static/aircox/main.js b/aircox/static/aircox/main.js index 91cd3bd..7b0ae3c 100644 --- a/aircox/static/aircox/main.js +++ b/aircox/static/aircox/main.js @@ -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& ***! diff --git a/aircox/static/aircox/vendor.js b/aircox/static/aircox/vendor.js index dc2f41b..c2ed976 100644 --- a/aircox/static/aircox/vendor.js +++ b/aircox/static/aircox/vendor.js @@ -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 ***! diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 79eb850..1812fdb 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -14,6 +14,7 @@ Context: {% block assets %} + {% endblock %} @@ -80,6 +81,9 @@ Context: {% endif %} + +
+ {% include "aircox/player.html" %} diff --git a/aircox/templates/aircox/log_item.html b/aircox/templates/aircox/log_item.html index 0cf518e..b6e8a9d 100644 --- a/aircox/templates/aircox/log_item.html +++ b/aircox/templates/aircox/log_item.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 %} - -{{ track.title }} - -— {{ track.artist }} -{% if track.info %}({{ track.info }}){% endif %} - -{% 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 %} + + {{ track.title }} + + — {{ track.artist }} + {% if track.info %}({{ track.info }}){% endif %} + + {% endwith %} +{% endif %} diff --git a/aircox/templates/aircox/log_list.html b/aircox/templates/aircox/log_list.html index 4fa4830..2a81cda 100644 --- a/aircox/templates/aircox/log_list.html +++ b/aircox/templates/aircox/log_list.html @@ -33,25 +33,18 @@ {% for object in object_list reversed %} - {% if object|is_diffusion %} - {% with object as diffusion %} - {% with diffusion.episode as object %} - - {% endwith %} - {% endwith %} - {% else %} - - {% endif %} {% endfor %}
+ {% if object|is_diffusion %} - {% include "aircox/episode_item.html" %} + {% else %} + {% endif %} {% include "aircox/log_item.html" %}
diff --git a/aircox/templates/aircox/page_list.html b/aircox/templates/aircox/page_list.html index 28352d5..aff0d98 100644 --- a/aircox/templates/aircox/page_list.html +++ b/aircox/templates/aircox/page_list.html @@ -49,8 +49,6 @@ - - {% endblock %} diff --git a/aircox/urls.py b/aircox/urls.py index 923c6d5..29e4792 100755 --- a/aircox/urls.py +++ b/aircox/urls.py @@ -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/'), diff --git a/aircox/views/__init__.py b/aircox/views/__init__.py index 430f02a..3a1f624 100644 --- a/aircox/views/__init__.py +++ b/aircox/views/__init__.py @@ -1,3 +1,5 @@ +from . import api + from .article import ArticleListView from .base import BaseView from .episode import EpisodeDetailView, EpisodeListView, TimetableView diff --git a/aircox/views/base.py b/aircox/views/base.py index 3bec93a..4409989 100644 --- a/aircox/views/base.py +++ b/aircox/views/base.py @@ -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) diff --git a/aircox/views/log.py b/aircox/views/log.py index a886781..cee59f3 100644 --- a/aircox/views/log.py +++ b/aircox/views/log.py @@ -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 ) + + diff --git a/assets/index.js b/assets/index.js index a52ee43..1abf65e 100644 --- a/assets/index.js +++ b/assets/index.js @@ -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'; diff --git a/assets/js/index.js b/assets/js/index.js index 215a588..78e99d6 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -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); + diff --git a/assets/styles.scss b/assets/styles.scss index ec5f28a..a65725e 100644 --- a/assets/styles.scss +++ b/assets/styles.scss @@ -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; } diff --git a/assets/vue/index.js b/assets/vue/index.js index a9ea751..ea8d98f 100644 --- a/assets/vue/index.js +++ b/assets/vue/index.js @@ -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}; diff --git a/assets/vue/onAir.vue b/assets/vue/onAir.vue new file mode 100644 index 0000000..d8ad445 --- /dev/null +++ b/assets/vue/onAir.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/assets/vue/player.vue b/assets/vue/player.vue new file mode 100644 index 0000000..268a1d3 --- /dev/null +++ b/assets/vue/player.vue @@ -0,0 +1,82 @@ + + + + + + + + diff --git a/notes.md b/notes.md index a2d4420..94aa64a 100755 --- a/notes.md +++ b/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: diff --git a/webpack.config.js b/webpack.config.js index 728c024..f5d08de 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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',