Compare commits

..

7 Commits

Author SHA1 Message Date
bkfox
a87b9c7e72 last sound logs + assets 2022-05-21 19:11:56 +02:00
bkfox
59d5a1c3dc wrong diffusion 2022-05-21 17:07:16 +02:00
bkfox
094e0ef1d2 translation 2022-05-21 16:50:57 +02:00
bkfox
669d26600a translation 2022-05-21 16:48:38 +02:00
bkfox
cf02bce45e translation 2022-05-21 16:40:04 +02:00
bkfox
478ce58c17 translation 2022-05-21 16:37:11 +02:00
Thomas Kairos
8f0dd9d248 Merge pull request '#41: Logs/À l'antenne: tracks manquantes' (#49) from fix-1.0-41 into develop-1.0
Reviewed-on: #49
2022-05-21 15:42:30 +02:00
12 changed files with 396 additions and 306 deletions

View File

@ -2,16 +2,17 @@
Platform to manage a radio, schedules, website, and so on. We use the power of great tools like Django or Liquidsoap.
This project is distributed under GPL version 3. More information in the LICENSE file, except for some files whose license is indicated.
This project is distributed under GPL version 3. More information in the LICENSE file, except for some files whose license is indicated inside source code.
## Features
* **streams**: multiple random music streams when no program is played. We also can specify a time range and frequency for each;
* **diffusions**: generate diffusions time slot for programs that have schedule informations. Check for conflicts and rerun.
* **liquidsoap**: create a configuration to use liquidsoap as a stream generator. Also provides interface and control to it;
* **sounds**: each programs have a folder where sounds can be put, that will be detected by the system. Quality can be check and reported for later use. Later, we plan to have uploaders to external plateforms. Sounds can be defined as excerpts or as archives.
* **cms**: application that can be used as basis for website;
* **sounds**: each programs have a folder for its podcast. Aircox detects updates, can run quality check, import related playlist (timestamped or position in track list). Sounds can be defined as excerpts or as archives.
* **log**: keep a trace of every played/loaded sounds on the stream generator.
* **admin**: admin user interface.
* **cms**: content management system.
## Scripts
@ -27,8 +28,6 @@ and `gunicorn` in mind.
## Installation
Later we plan to have an installation script to reduce the number of above steps.
### Dependencies
For python dependencies take a peek at the `requirements.txt` file, plus
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
@ -50,7 +49,7 @@ Development dependencies:
All scripts and files assumes that:
- you have cloned aircox in `/srv/apps/` (such as `/srv/apps/aircox/README.md`)
- you have a supervisor running (we have scripts for `supervisord`)
- you want to use `gunicorn` as WSGI server (otherwise, you'll need to remove it from the requirement list)
- you use `gunicorn` as WSGI server (otherwise, you'll need to remove it from the requirement list)
This installation process uses a virtualenv, including all provided scripts.
@ -87,8 +86,7 @@ server from this directory:
./manage.py runserver
```
You can access to the django admin interface at `http://127.0.0.1:8000/admin`
and to the cms interface at `http://127.0.0.1:8000/cms/`.
You can access to the django admin interface at `http://127.0.0.1:8000/admin`.
From the admin interface:
* create a Station
@ -96,8 +94,6 @@ From the admin interface:
* defines Outputs for the streamer (look at Liquidsoap documentation for
more information on how to configure it)
TODO: cms related documentation here
Once the configuration is okay, you must start the *controllers monitor*,
that creates configuration file for the audio streams using the new information
and that runs the appropriate application (note that you dont need to restart it
@ -107,5 +103,5 @@ If you use supervisord and our script with it, you can use the services defined
in it instead of running commands manually.
## More informations
There are extra informations in `aircox/README.md`.
There are extra informations in `aircox/README.md` and `aircox_streamer/README.md`.

View File

@ -6,13 +6,11 @@ A Station contains programs that can be scheduled or streamed. A *Scheduled Prog
Each program has a directory on the server where user puts its podcasts (in **AIRCOX_PROGRAM_DIR**). It contains the directories **archives** (complete show's podcasts) and **excerpts** (partial or whatever podcasts).
## manage.py's commands
* **diffusions**: update/create, check and clean diffusions based on programs schedules;
* **import_playlist**: import a playlist from a csv file, and associate it to a sound;
* **sound_monitor**: check for existing and missing sounds files in programs directories and synchronize the database. It can check for the quality of file and update sound info.
* **sound_quality_check**: check for the quality of the file (don't update database);
* **streamer**: audio stream generation and control it;
* `diffusions`: update/create, check and clean diffusions based on programs schedules;
* `import_playlist`: import a playlist from a csv file, and associate it to a sound;
* `sounds_monitor`: check for existing and missing sounds files in programs directories and synchronize the database. It can check for the quality of file and update sound info.
* `sounds_quality_check`: check for the quality of the file (don't update database);
## Requirements

File diff suppressed because it is too large Load Diff

View File

@ -466,13 +466,13 @@ class Stream(models.Model):
)
begin = models.TimeField(
_('begin'), blank=True, null=True,
help_text=_('used to define a time range this stream is'
help_text=_('used to define a time range this stream is '
'played')
)
end = models.TimeField(
_('end'),
blank=True, null=True,
help_text=_('used to define a time range this stream is'
help_text=_('used to define a time range this stream is '
'played')
)

View File

@ -255,7 +255,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _for
\*********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ Live; }\n/* harmony export */ });\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n\n\nclass Live {\n constructor({\n url,\n timeout = 10,\n src = \"\"\n } = {}) {\n this.url = url;\n this.timeout = timeout;\n this.src = src;\n this.interval = null;\n this.promise = null;\n this.items = [];\n this.current = null;\n } //-- data refreshing\n\n\n drop() {\n this.promise = null;\n }\n /**\n * Fetch data from server.\n *\n * @param {Object} options\n * @param {Function} options.then: call this method on fetch, `this` passed as argument.\n * @return {Promise} Promise resolving to fetched items.\n */\n\n\n fetch({\n then = null\n } = {}) {\n const promise = fetch(this.url).then(response => response.ok ? response.json() : Promise.reject(response)).then(data => {\n this.items = data;\n let item = this.items && this.items[this.items.length - 1];\n\n if (item) {\n item.src = this.src;\n this.current = new _model__WEBPACK_IMPORTED_MODULE_1__[\"default\"](item);\n } else this.current = null;\n\n if (then) then(this);\n return this.items;\n });\n this.promise = promise;\n return promise;\n }\n\n _refresh(options = {}) {\n const promise = this.fetch(options);\n promise.then(() => {\n if (promise != this.promise) return [];\n });\n return promise;\n }\n /**\n * Refresh live info every `this.timeout`.\n * @param {Object} options: arguments passed to `this.fetch`.\n */\n\n\n refresh(options = {}) {\n if (this.interval !== null) return;\n\n this._refresh(options);\n\n this.interval = (0,_utils__WEBPACK_IMPORTED_MODULE_0__.setEcoInterval)(() => this._refresh(options), this.timeout * 1000);\n return this.interval;\n }\n\n stopRefresh() {\n this.interval !== null && clearInterval(this.interval);\n }\n\n}\n\n//# sourceURL=webpack://aircox-assets/./src/live.js?");
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ Live; }\n/* harmony export */ });\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n\n\nclass Live {\n constructor({\n url,\n timeout = 10,\n src = \"\"\n } = {}) {\n this.url = url;\n this.timeout = timeout;\n this.src = src;\n this.interval = null;\n this.promise = null;\n this.items = [];\n this.current = null;\n } //-- data refreshing\n\n\n drop() {\n this.promise = null;\n }\n /**\n * Fetch data from server.\n *\n * @param {Object} options\n * @param {Function} options.then: call this method on fetch, `this` passed as argument.\n * @return {Promise} Promise resolving to fetched items.\n */\n\n\n fetch({\n then = null\n } = {}) {\n const promise = fetch(this.url).then(response => response.ok ? response.json() : Promise.reject(response)).then(data => {\n data.forEach(item => {\n if (item.start) item.start = new Date(item.start);\n if (item.end) item.end = new Date(item.end);\n });\n this.items = data;\n const now = new Date();\n let item = data.find(it => it.start && it.start <= now < it.end) || data.length ? data[data.length - 1] : null;\n\n if (item) {\n item.src = this.src;\n this.current = new _model__WEBPACK_IMPORTED_MODULE_1__[\"default\"](item);\n } else this.current = null;\n\n if (then) then(this);\n return this.items;\n });\n this.promise = promise;\n return promise;\n }\n\n _refresh(options = {}) {\n const promise = this.fetch(options);\n promise.then(() => {\n if (promise != this.promise) return [];\n });\n return promise;\n }\n /**\n * Refresh live info every `this.timeout`.\n * @param {Object} options: arguments passed to `this.fetch`.\n */\n\n\n refresh(options = {}) {\n if (this.interval !== null) return;\n\n this._refresh(options);\n\n this.interval = (0,_utils__WEBPACK_IMPORTED_MODULE_0__.setEcoInterval)(() => this._refresh(options), this.timeout * 1000);\n return this.interval;\n }\n\n stopRefresh() {\n this.interval !== null && clearInterval(this.interval);\n }\n\n}\n\n//# sourceURL=webpack://aircox-assets/./src/live.js?");
/***/ }),

View File

@ -1,6 +1,5 @@
{% extends "./filter.html" %}
{% load static %}
{% load static i18n %}
{% block content %}
<ul>

View File

@ -1,4 +1,4 @@
{% load i18n %}
<h3>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</h3>
<h3>{{ title }}</h3>
{% block content %}{% endblock %}

18
aircox_streamer/README.md Normal file
View File

@ -0,0 +1,18 @@
# Aircox Streamer
This application handles interfacing Aircox with Liquidsoap:
- generate for each station liquidsoap configuration files;
- handle played diffusions, sounds files and tracks;
- launch program's diffusion from sound files;
- provide admin interface and API in order to control liquidsoap;
## Architecture
`aircox_streamer` Django application provides management command `streamer`, which
can be run in virtualenv from shell: ``./manage.py streamer``.
This application allows to:
- launch Liquidsoap;
- generate config file and playlists: regular Django template file in `scripts/station.liq`;
- monitor what is being played and what has to be played using Telnet to communicate
with Liquidsoap process;

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-31 18:34+0000\n"
"POT-Creation-Date: 2022-05-21 14:30+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,91 +18,91 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: controllers.py:75
#: aircox_streamer/controllers.py:75
msgid "playing"
msgstr "en cours de lecture"
#: controllers.py:77
#: aircox_streamer/controllers.py:77
msgid "paused"
msgstr "pause"
#: controllers.py:79
#: aircox_streamer/controllers.py:79
msgid "stopped"
msgstr "arrêt"
#: templates/aircox_streamer/source_item.html:18
#: aircox_streamer/templates/aircox_streamer/source_item.html:18
msgid "Edit related program"
msgstr "Éditer le programme correspondant"
#: templates/aircox_streamer/source_item.html:27
#: aircox_streamer/templates/aircox_streamer/source_item.html:27
msgid "Synchronize source with Liquidsoap"
msgstr "Synchroniser la source avec Liquidsoap"
#: templates/aircox_streamer/source_item.html:31
#: aircox_streamer/templates/aircox_streamer/source_item.html:31
msgid "Synchronise"
msgstr "Synchroniser"
#: templates/aircox_streamer/source_item.html:34
#: aircox_streamer/templates/aircox_streamer/source_item.html:34
msgid "Restart current track"
msgstr "Rejouer le morceau en cours"
#: templates/aircox_streamer/source_item.html:38
#: aircox_streamer/templates/aircox_streamer/source_item.html:38
msgid "Restart"
msgstr "Rejouer"
#: templates/aircox_streamer/source_item.html:41
#: aircox_streamer/templates/aircox_streamer/source_item.html:41
msgid "Skip current file"
msgstr "Passer le fichier actuel"
#: templates/aircox_streamer/source_item.html:42
#: aircox_streamer/templates/aircox_streamer/source_item.html:42
msgid "Skip"
msgstr "Passer"
#: templates/aircox_streamer/source_item.html:51
#: aircox_streamer/templates/aircox_streamer/source_item.html:51
msgid "Add sound"
msgstr "Ajouter un son"
#: templates/aircox_streamer/source_item.html:59
#: aircox_streamer/templates/aircox_streamer/source_item.html:58
msgid "Select a sound"
msgstr "Sélectionner un son"
#: templates/aircox_streamer/source_item.html:61
msgid "Add a sound to the queue (queue may start playing)"
msgstr "Ajouter un son à la file de lecture (la file de lecture peut démarer)"
#: templates/aircox_streamer/source_item.html:70
#: aircox_streamer/templates/aircox_streamer/source_item.html:69
msgid "Add"
msgstr "Ajouter"
#: templates/aircox_streamer/source_item.html:76
#: aircox_streamer/templates/aircox_streamer/source_item.html:74
msgid "Add a sound to the queue (queue may start playing)"
msgstr "Ajouter un son à la file de lecture (la file de lecture peut démarer)"
#: aircox_streamer/templates/aircox_streamer/source_item.html:80
msgid "Sounds in queue"
msgstr "Sons dans la file de lecture"
#: templates/aircox_streamer/source_item.html:94
#: aircox_streamer/templates/aircox_streamer/source_item.html:98
msgid "Status"
msgstr "Statut"
#: templates/aircox_streamer/source_item.html:104
#: aircox_streamer/templates/aircox_streamer/source_item.html:108
msgid "Air time"
msgstr "En antenne depuis"
#: templates/aircox_streamer/source_item.html:114
#: aircox_streamer/templates/aircox_streamer/source_item.html:118
msgid "Time left"
msgstr "Temps restant"
#: templates/aircox_streamer/source_item.html:122
#: aircox_streamer/templates/aircox_streamer/source_item.html:126
msgid "Data source"
msgstr "Source de donnée"
#: templates/aircox_streamer/streamer.html:19
#: aircox_streamer/templates/aircox_streamer/streamer.html:23
msgid "Reload"
msgstr "Recharger"
#: templates/aircox_streamer/streamer.html:26
#: templates/aircox_streamer/streamer.html:27
#: aircox_streamer/templates/aircox_streamer/streamer.html:30
#: aircox_streamer/templates/aircox_streamer/streamer.html:31
msgid "Select a station"
msgstr "Sélectionner une station"
#: urls.py:9 views.py:9
#: aircox_streamer/urls.py:10 aircox_streamer/views.py:9
msgid "Streamer Monitor"
msgstr "Moniteur de stream"

View File

@ -55,7 +55,9 @@ class Monitor:
sync_timeout = 5
""" Timeout in minutes between two streamer's sync. """
sync_next = None
""" Datetime of the next sync """
""" Datetime of the next sync. """
last_sound_logs = None
""" Last logged sounds, as ``{source_id: log}``. """
@property
def station(self):
@ -78,12 +80,21 @@ class Monitor:
self.cancel_timeout = cancel_timeout
self.__dict__.update(kwargs)
self.logs = self.get_logs_queryset()
self.init_last_sound_logs()
def get_logs_queryset(self):
""" Return queryset to assign as `self.logs` """
return self.station.log_set.select_related('diffusion', 'sound') \
return self.station.log_set.select_related('diffusion', 'sound', 'track') \
.order_by('-pk')
def init_last_sound_logs(self, key=None):
""" Retrieve last logs and initialize `last_sound_logs` """
logs = {}
for source in self.streamer.sources:
qs = self.logs.filter(source=source.id, sound__isnull=False)
logs[source.id] = qs.first()
self.last_sound_logs = logs
def monitor(self):
""" Run all monitoring functions once. """
if not self.streamer.is_ready:
@ -122,36 +133,37 @@ class Monitor:
self.handle_diffusions()
self.sync()
__last_log_kwargs = None
def log(self, **kwargs):
def log(self, source, **kwargs):
""" Create a log using **kwargs, and print info """
kwargs.setdefault('station', self.station)
kwargs.setdefault('date', tz.now())
if self.__last_log_kwargs == kwargs:
return
self.__last_log_kwargs = kwargs
log = Log(**kwargs)
log = Log(source=source, **kwargs)
log.save()
log.print()
if log.sound:
self.last_sound_logs[source] = log
return log
def trace_sound(self, source):
""" Return on air sound log (create if not present). """
air_uri, air_time = source.uri, source.air_time
last_log = self.last_sound_logs.get(source.id)
if last_log and last_log.sound.file.path == source.uri:
return last_log
# FIXME: can be a sound played when no Sound instance? If not, remove
# comment.
# check if there is yet a log for this sound on the source
log = self.logs.on_air().filter(
Q(sound__file=air_uri) |
# sound can be null when arbitrary sound file is played
Q(sound__isnull=True, track__isnull=True, comment=air_uri),
source=source.id,
date__range=date_range(air_time, self.delay),
).first()
if log:
return log
# log = self.logs.on_air().filter(
# Q(sound__file=air_uri) |
# # sound can be null when arbitrary sound file is played
# Q(sound__isnull=True, track__isnull=True, comment=air_uri),
# source=source.id,
# date__range=date_range(air_time, self.delay),
# ).first()
# if log:
# return log
# get sound
diff = None
@ -185,7 +197,6 @@ class Monitor:
pos = log.date + tz.timedelta(seconds=track.timestamp)
if pos > now:
break
# log track on air
self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source,
track=track, comment=track)

View File

@ -30,9 +30,15 @@ export default class Live {
response.ok ? response.json()
: Promise.reject(response)
).then(data => {
data.forEach(item => {
if(item.start) item.start = new Date(item.start)
if(item.end) item.end = new Date(item.end)
})
this.items = data
let item = this.items && this.items[this.items.length-1]
const now = new Date()
let item = data.find(it => it.start && (it.start <= now < it.end)) ||
data.length ? data[data.length-1] : null;
if(item) {
item.src = this.src
this.current = new Model(item)