From 5e364f0814ba8ee2bcac2630da61d38b46f76c1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:50:46 +0000 Subject: [PATCH 01/40] Bump websockets from 8.1 to 9.1 in /build Bumps [websockets](https://github.com/aaugustin/websockets) from 8.1 to 9.1. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Changelog](https://github.com/aaugustin/websockets/blob/9.1/docs/changelog.rst) - [Commits](https://github.com/aaugustin/websockets/compare/8.1...9.1) --- updated-dependencies: - dependency-name: websockets dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/requirements.txt b/build/requirements.txt index 1d8b09f..17487be 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -8,7 +8,7 @@ mutagen==1.45.1 sounddevice==0.4.2 setproctitle==1.2.2 pyttsx3==2.90 -websockets==8.1 +websockets==9.1 typing_extensions==3.10.0.0 pyserial==3.5 requests==2.26.0 From ac5409587d4ef9056cb290eef3ced675ab4fc82f Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 11 Sep 2021 23:44:15 +0100 Subject: [PATCH 02/40] Add WIP UI and manager for alerts. --- baps_types/alert.py | 65 ++++++++++++++++++++++++++++++++++++++++ helpers/alert_manager.py | 29 ++++++++++++++++++ ui-templates/alerts.html | 41 +++++++++++++++++++++++++ ui-templates/index.html | 20 +++++++++++++ web_server.py | 20 ++++++++++++- 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 baps_types/alert.py create mode 100644 helpers/alert_manager.py create mode 100644 ui-templates/alerts.html diff --git a/baps_types/alert.py b/baps_types/alert.py new file mode 100644 index 0000000..493a544 --- /dev/null +++ b/baps_types/alert.py @@ -0,0 +1,65 @@ +from typing import Any, Dict +from datetime import datetime + +CRITICAL = "Critical" +WARNING = "Warning" + +class Alert: + start_time: int = 0 + last_time: int = 0 + end_time: int = -1 + id: str + title: str + description: str + module: str + severity: str + + + @property + def ui_class(self) -> str: + if self.severity == CRITICAL: + return "danger" + if self.severity == WARNING: + return "warning" + return "info" + + # return self._weight + + # weight.setter + # def weight(self, value: int): + # self._weight = value + + + @property + def __dict__(self): + attrs = ["start_time", "last_time", "end_time", "id", "title", "description", "module", "severity"] + out = {} + for attr in attrs: + out[attr] = self.__getattribute__(attr) + + return out + + def __init__(self, new_data: Dict[str,Any]): + required_vars = [ + "start_time", # Just in case an alert wants to show starting earlier than it is reported. + "id", + "title", + "description", + "module", + "severity" + ] + + for key in required_vars: + if key not in new_data.keys(): + raise KeyError("Key {} is missing from data to create Alert.".format(key)) + + #if type(new_data[key]) != type(getattr(self,key)): + # raise TypeError("Key {} has type {}, was expecting {}.".format(key, type(new_data[key]), type(getattr(self,key)))) + + # Account for if the creator didn't want to set a custom time. + if key == "start_time" and new_data[key] == -1: + new_data[key] = datetime.now() + + setattr(self,key,new_data[key]) + + self.last_time = self.start_time diff --git a/helpers/alert_manager.py b/helpers/alert_manager.py new file mode 100644 index 0000000..820f45d --- /dev/null +++ b/helpers/alert_manager.py @@ -0,0 +1,29 @@ +from typing import List +from baps_types.alert import CRITICAL, Alert + +class AlertManager(): + _alerts: List[Alert] + + def __init__(self): + self._alerts = [Alert( + { + "start_time": -1, + "id": "test", + "title": "Test Alert", + "description": "This is a test alert.", + "module": "Test", + "severity": CRITICAL + } + )] + + @property + def alerts_current(self): + return self._alerts + + @property + def alert_count_current(self): + return len(self._alerts) + + @property + def alert_count_previous(self): + return len(self._alerts) diff --git a/ui-templates/alerts.html b/ui-templates/alerts.html new file mode 100644 index 0000000..edf1b7e --- /dev/null +++ b/ui-templates/alerts.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% block head %} + +{% endblock %} +{% block content_inner %} + {% if data %} +

Current Alerts: {{ data.alert_count_current }}

+ +
+ {% for alert in data.alerts_current %} +
+
+

+ + {{ alert.severity }} +

+ Since {{ alert.start_time }} + Last Seen {{ alert.last_time }} + {% if alert.end_time > -1 %} + Ended {{ alert.end_time }} + {% endif %} +
+ +
+
+ Module: {{ alert.module }}
+ {{ alert.description }} +
+
+
+ {% endfor %} +
+
+

Previous Alerts: {{ data.alert_count_previous }}

+
+ +
+ {% endif %} +{% endblock %} diff --git a/ui-templates/index.html b/ui-templates/index.html index 53c5aa4..bb8b21b 100644 --- a/ui-templates/index.html +++ b/ui-templates/index.html @@ -1,4 +1,7 @@ {% extends 'base.html' %} +{% block head %} + +{% endblock %} {% block content %}
@@ -16,6 +19,23 @@ Open BAPS Presenter + +
+ {% if data.alert_count > 0 %} + diff --git a/ui-templates/index.html b/ui-templates/index.html index bb8b21b..fe62487 100644 --- a/ui-templates/index.html +++ b/ui-templates/index.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% block head %} - + {% endblock %} {% block content %} From f1e04c3d8a6e2020c5aea3a576e61cde040391ad Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 16 Sep 2021 23:42:20 +0100 Subject: [PATCH 06/40] Switch sanic debug to follow package.BETA --- web_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_server.py b/web_server.py index 85ee75e..a46dc8e 100644 --- a/web_server.py +++ b/web_server.py @@ -17,7 +17,6 @@ import json import os from helpers.os_environment import ( - isBundelled, resolve_external_file_path, resolve_local_file_path, ) @@ -28,6 +27,7 @@ from helpers.the_terminator import Terminator from helpers.normalisation import get_normalised_filename_if_available from helpers.myradio_api import MyRadioAPI from helpers.alert_manager import AlertManager +import package env = Environment( loader=FileSystemLoader("%s/ui-templates/" % os.path.dirname(__file__)), @@ -525,9 +525,9 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana app.run( host=server_state.get()["host"], port=server_state.get()["port"], - debug=(not isBundelled()), auto_reload=False, - access_log=(not isBundelled()), + debug=not package.BETA, + access_log=not package.BETA, ) ) except Exception: From 7dc0facf73295702e723795cf99ae75f0b0ea389 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 21 Sep 2021 22:49:05 +0100 Subject: [PATCH 07/40] Add basic player restart alerts --- alerts/player.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ player.py | 5 ++- 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 alerts/player.py diff --git a/alerts/player.py b/alerts/player.py new file mode 100644 index 0000000..45184c1 --- /dev/null +++ b/alerts/player.py @@ -0,0 +1,93 @@ +# Any alerts produced by the player.py instances. +import json +from typing import Any, Dict, List, Optional +from datetime import datetime, timedelta +from helpers.os_environment import resolve_external_file_path +from helpers.alert_manager import AlertProvider +from baps_types.alert import CRITICAL, WARNING, Alert + +MODULE = "Player" # This should match the log file, so the UI will link to the logs page. + +class PlayerAlertProvider(AlertProvider): + + _server_state: Dict[str, Any] + _states: List[Optional[Dict[str,Any]]] = [] + _player_count: int + + def __init__(self): + # Player count only changes after server restart, may as well just load this once. + with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: + self._server_state = json.loads(file.read()) + + self._player_count = int(self._server_state["num_channels"]) + self._states = [None] * self._player_count + + # To simplify monitoring (and allow detection of things going super weird), we are going to read from the state file to work out the alerts. + def get_alerts(self): + for channel in range(self._player_count): + with open(resolve_external_file_path("state/Player{}.json".format(channel))) as file: + self._states[channel] = json.loads(file.read()) + + funcs = [self._channel_count, self._initialised, self._start_time] + + alerts: List[Alert] = [] + + for func in funcs: + func_alerts = func() + if func_alerts: + alerts.extend(func_alerts) + + return alerts + + def _channel_count(self): + if self._player_count <= 0: + return [Alert({ + "start_time": -1, # Now + "id": "no_channels", + "title": "There are no players configured.", + "description": "The number of channels configured is {}. Please set to at least 1 on the 'Server Config' page.".format(self._player_count), + "module": MODULE+"Handler", + "severity": CRITICAL + })] + + def _initialised(self): + alerts: List[Alert] = [] + for channel in range(self._player_count): + if self._states[channel] and not self._states[channel]["initialised"]: + alerts.append(Alert({ + "start_time": -1, # Now + "id": "player_{}_not_initialised".format(channel), + "title": "Player {} is not initialised.".format(channel), + "description": "This typically means the player channel was not able find the configured sound output on the system. Please check the 'Player Config' and Player logs to determine the cause.", + "module": MODULE+str(channel), + "severity": CRITICAL + })) + return alerts + + def _start_time(self): + server_start_time = self._server_state["start_time"] + server_start_time = datetime.fromtimestamp(server_start_time) + delta = timedelta( + seconds=30, + ) + + alerts: List[Alert] = [] + for channel in range(self._player_count): + start_time = self._states[channel]["start_time"] + start_time = datetime.fromtimestamp(start_time) + if (start_time > server_start_time + delta): + alerts.append(Alert({ + "start_time": -1, + "id": "player_{}_restarted".format(channel), + "title": "Player {} restarted after the server started.".format(channel), + "description": +"""Player {} last restarted at {}, after the server first started at {}, suggesting a failure. + +This likely means there was an unhandled exception in the player code, causing the server to restart the player. + +Please check player logs to investigate the cause. Please restart the server to clear this warning.""" + .format(channel, str(start_time).rsplit(".",1)[0], str(server_start_time).rsplit(".",1)[0]), + "module": MODULE+str(channel), + "severity": WARNING + })) + return alerts diff --git a/player.py b/player.py index 2f70f31..5230432 100644 --- a/player.py +++ b/player.py @@ -35,6 +35,7 @@ from pygame import mixer from mutagen.mp3 import MP3 from syncer import sync from threading import Timer +from datetime import datetime from helpers.normalisation import get_normalised_filename_if_available from helpers.myradio_api import MyRadioAPI @@ -969,7 +970,7 @@ class Player: self.out_q = out_q self.logger = LoggingManager( - "Player" + str(channel), debug=package.build_beta) + "Player" + str(channel), debug=package.BETA) self.api = MyRadioAPI(self.logger, server_state) @@ -980,6 +981,8 @@ class Player: self.__rate_limited_params, ) + self.state.update("start_time", datetime.now().timestamp()) + self.state.add_callback(self._send_status) self.state.update("channel", channel) From 642aaf04d599a63114c17ddd4896f1667338a985 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 21 Sep 2021 22:50:52 +0100 Subject: [PATCH 08/40] Remove channel states from alerts page info --- web_server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web_server.py b/web_server.py index a46dc8e..89d17f8 100644 --- a/web_server.py +++ b/web_server.py @@ -144,10 +144,6 @@ def ui_status(request): @app.route("/alerts") def ui_alerts(request): - channel_states = [] - for i in range(server_state.get()["num_channels"]): - channel_states.append(status(i)) - data = { "alerts_current": alerts.alerts_current, "alerts_count_current": alerts.alert_count_current, From 4e9a1c4b6e29c2259e96ffd2f6d5eaf7efc60695 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 21 Sep 2021 22:52:27 +0100 Subject: [PATCH 09/40] Switch to quit. --- ui-templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-templates/index.html b/ui-templates/index.html index fe62487..52ebd66 100644 --- a/ui-templates/index.html +++ b/ui-templates/index.html @@ -33,7 +33,7 @@
Logs Restart - Shutdown + Quit

From 382de936a336896e88998f5f9948a2bcb940c4fd Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 21 Sep 2021 23:02:24 +0100 Subject: [PATCH 10/40] Allow player config to better handle borked players. --- ui-templates/config_player.html | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ui-templates/config_player.html b/ui-templates/config_player.html index 1c09a90..78d2c79 100644 --- a/ui-templates/config_player.html +++ b/ui-templates/config_player.html @@ -9,21 +9,25 @@

Currently Selected

{% for channel in data.channels %} - Player {{channel.channel}}: - {% if channel.output %} - {{channel.output}} + Player {{loop.index0}}: + {% if channel %} + {% if channel.output %} + {{channel.output}} + {% else %} + Default Audio Device + {% endif %} + {% if not channel.initialised %} + - ERROR: Player did not start, potentially configured sound output is missing? Check Alerts. + {% endif %} {% else %} - Default Audio Device - {% endif %} - {% if not channel.initialised %} - - ERROR: Player did not start, potentially missing sound output? + - ERROR: Player did not respond, likely it is dead :/ Check Alerts. {% endif %}
{% endfor %}
Set for: -{% for channel in data.channels %} +{% for channel in data.channels if channel %} Player {{channel.channel}} / {% endfor %} Default Audio Output @@ -37,7 +41,9 @@ Default Audio Output {% if host_api.usable %} Set for: {% for channel in data.channels %} - {% if channel.output == output.name %} + {% if not channel %} + Player {{loop.index0}} + {% elif channel.output == output.name %} Player {{channel.channel}} {% else %} Player {{channel.channel}} From bc2e60cdc9c33820b4bba95de2b0fda856aa2304 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 22 Sep 2021 00:15:17 +0100 Subject: [PATCH 11/40] Update alerts to actually update/end, move list to macro. --- baps_types/alert.py | 20 +++++++++------ helpers/alert_manager.py | 39 +++++++++++++++++++++--------- ui-templates/alerts.html | 35 ++++----------------------- ui-templates/parts/alert_list.html | 31 ++++++++++++++++++++++++ web_server.py | 4 +-- 5 files changed, 77 insertions(+), 52 deletions(-) create mode 100644 ui-templates/parts/alert_list.html diff --git a/baps_types/alert.py b/baps_types/alert.py index 493a544..f6958fc 100644 --- a/baps_types/alert.py +++ b/baps_types/alert.py @@ -1,13 +1,13 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional from datetime import datetime CRITICAL = "Critical" WARNING = "Warning" class Alert: - start_time: int = 0 - last_time: int = 0 - end_time: int = -1 + start_time: datetime + last_time: datetime + end_time: Optional[datetime] id: str title: str description: str @@ -23,11 +23,14 @@ class Alert: return "warning" return "info" - # return self._weight + # This alert has happened again. + def reoccured(self): + self.last_time = datetime.now() + self.end_time = None - # weight.setter - # def weight(self, value: int): - # self._weight = value + # This alert has finished, just update end time and keep last_time. + def cleared(self): + self.end_time = datetime.now() @property @@ -63,3 +66,4 @@ class Alert: setattr(self,key,new_data[key]) self.last_time = self.start_time + self.end_time = None diff --git a/helpers/alert_manager.py b/helpers/alert_manager.py index fea93ed..ab9c476 100644 --- a/helpers/alert_manager.py +++ b/helpers/alert_manager.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Optional #Magic for importing alert providers from alerts directory. from pkgutil import iter_modules @@ -54,25 +54,40 @@ class AlertManager(): def poll_alerts(self): # Poll modules for any alerts. - alerts: List[Alert] = [] + new_alerts: List[Optional[Alert]] = [] for provider in self._providers: provider_alerts = provider.get_alerts() if provider_alerts: - alerts.extend(provider_alerts) + new_alerts.extend(provider_alerts) - self._alerts = alerts + # Here we replace new firing alerts with older ones, to keep any context. + # (This doesn't do anything yet really, for future use.) + for existing in self._alerts: + found = False + for new in new_alerts: + # given we're removing alerts, got to skip any we removed. + if not new: + continue + + if existing.id == new.id: + # Alert is continuing. Replace it with the old one. + index = new_alerts.index(new) + existing.reoccured() + new_alerts[index] = None # We're going to merge the existing and new, so clear the new one out. + found = True + break + if found == False: + # The existing alert is gone, mark it as ended. + existing.cleared() + + self._alerts.extend([value for value in new_alerts if value]) # Remove any nulled out new alerts @property def alerts_current(self): self.poll_alerts() - return self._alerts + return [alert for alert in self._alerts if not alert.end_time] @property - def alert_count_current(self): + def alerts_previous(self): self.poll_alerts() - return len(self._alerts) - - @property - def alert_count_previous(self): - self.poll_alerts() - return len(self._alerts) + return [alert for alert in self._alerts if alert.end_time] diff --git a/ui-templates/alerts.html b/ui-templates/alerts.html index ff100c0..36e5fdd 100644 --- a/ui-templates/alerts.html +++ b/ui-templates/alerts.html @@ -1,43 +1,18 @@ {% extends 'base.html' %} +{% from 'parts/alert_list.html' import alert_list %} {% block head %} {% endblock %} {% block content_inner %} {% if data %}

Current Alerts: {{ data.alert_count_current }}

-
- {% for alert in data.alerts_current %} -
-
-

- - {{ alert.severity }} -

- Since {{ alert.start_time }} - Last Seen {{ alert.last_time }} - {% if alert.end_time > -1 %} - Ended {{ alert.end_time }} - {% endif %} -
- -
-
- Module: {{ alert.module }} - {% autoescape false %} -

{{ alert.description | replace("\n\n", "

") | replace("\n", "
")}}

- {% endautoescape %} -
-
-
- {% endfor %} + {{ alert_list(data.alerts_current) }}

Previous Alerts: {{ data.alert_count_previous }}

-
- -
+
+ {{ alert_list(data.alerts_previous) }} +
{% endif %} {% endblock %} diff --git a/ui-templates/parts/alert_list.html b/ui-templates/parts/alert_list.html new file mode 100644 index 0000000..6c8ae48 --- /dev/null +++ b/ui-templates/parts/alert_list.html @@ -0,0 +1,31 @@ +{% macro alert_list(alerts) %} + {% for alert in alerts %} +
+
+

+ + {{ alert.severity }} +

+ Since {{ alert.start_time }} + Last Seen {{ alert.last_time }} + {% if alert.end_time %} + Ended {{ alert.end_time }} + {% endif %} +
+ +
+
+ Module: {{ alert.module }} + {% autoescape false %} +

{{ alert.description | replace("\n\n", "

") | replace("\n", "
")}}

+ {% endautoescape %} +
+
+
+ {% endfor %} + {% if not alerts %} + No alerts here. + {% endif %} +{% endmacro %} diff --git a/web_server.py b/web_server.py index 89d17f8..08252a7 100644 --- a/web_server.py +++ b/web_server.py @@ -122,7 +122,7 @@ def ui_index(request): data = { "ui_page": "index", "ui_title": "", - "alert_count": alerts.alert_count_current, + "alert_count": len(alerts.alerts_current), "server_version": config["server_version"], "server_build": config["server_build"], "server_name": config["server_name"], @@ -146,7 +146,7 @@ def ui_status(request): def ui_alerts(request): data = { "alerts_current": alerts.alerts_current, - "alerts_count_current": alerts.alert_count_current, + "alerts_previous": alerts.alerts_previous, "ui_page": "alerts", "ui_title": "Alerts" } From b8c6f087c6381add5c5e645c0df58a20dca60fc6 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 22 Sep 2021 00:57:51 +0100 Subject: [PATCH 12/40] Tidy up alert rendering. --- alerts/player.py | 3 ++- alerts/server.py | 3 ++- baps_types/happytime.py | 3 +++ ui-templates/alerts.html | 12 ++++-------- ui-templates/parts/alert_list.html | 16 ++++++---------- web_server.py | 6 ++++++ 6 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 baps_types/happytime.py diff --git a/alerts/player.py b/alerts/player.py index 45184c1..ff1b437 100644 --- a/alerts/player.py +++ b/alerts/player.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from helpers.os_environment import resolve_external_file_path from helpers.alert_manager import AlertProvider from baps_types.alert import CRITICAL, WARNING, Alert +from baps_types.happytime import happytime MODULE = "Player" # This should match the log file, so the UI will link to the logs page. @@ -86,7 +87,7 @@ class PlayerAlertProvider(AlertProvider): This likely means there was an unhandled exception in the player code, causing the server to restart the player. Please check player logs to investigate the cause. Please restart the server to clear this warning.""" - .format(channel, str(start_time).rsplit(".",1)[0], str(server_start_time).rsplit(".",1)[0]), + .format(channel, happytime(start_time), happytime(server_start_time)), "module": MODULE+str(channel), "severity": WARNING })) diff --git a/alerts/server.py b/alerts/server.py index 7b56151..375f87d 100644 --- a/alerts/server.py +++ b/alerts/server.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from helpers.os_environment import resolve_external_file_path from helpers.alert_manager import AlertProvider from baps_types.alert import CRITICAL, WARNING, Alert +from baps_types.happytime import happytime MODULE = "BAPSicleServer" # This should match the log file, so the UI will link to the logs page. @@ -66,7 +67,7 @@ class ServerAlertProvider(AlertProvider): It may have been automatically restarted by the OS. If this is not expected, please check logs to investigate why BAPSicle restarted/crashed.""" - .format(str(start_time).rsplit(".",1)[0]), + .format(happytime(start_time)), "module": MODULE, "severity": WARNING })] diff --git a/baps_types/happytime.py b/baps_types/happytime.py new file mode 100644 index 0000000..898730e --- /dev/null +++ b/baps_types/happytime.py @@ -0,0 +1,3 @@ +from datetime import datetime +def happytime(date: datetime): + return date.strftime("%Y-%m-%d %H:%M:%S") diff --git a/ui-templates/alerts.html b/ui-templates/alerts.html index 36e5fdd..c90fd60 100644 --- a/ui-templates/alerts.html +++ b/ui-templates/alerts.html @@ -5,14 +5,10 @@ {% endblock %} {% block content_inner %} {% if data %} -

Current Alerts: {{ data.alert_count_current }}

-
- {{ alert_list(data.alerts_current) }} -
+

Current Alerts: {{ data.alerts_current | length }}

+ {{ alert_list(data.alerts_current) }}
-

Previous Alerts: {{ data.alert_count_previous }}

-
- {{ alert_list(data.alerts_previous) }} -
+

Previous Alerts: {{ data.alerts_previous | length }}

+ {{ alert_list(data.alerts_previous) }} {% endif %} {% endblock %} diff --git a/ui-templates/parts/alert_list.html b/ui-templates/parts/alert_list.html index 6c8ae48..c559d18 100644 --- a/ui-templates/parts/alert_list.html +++ b/ui-templates/parts/alert_list.html @@ -2,21 +2,17 @@ {% for alert in alerts %}
-

- - {{ alert.severity }} -

- Since {{ alert.start_time }} - Last Seen {{ alert.last_time }} + {{ alert.severity }} +

{{ alert.title }}

+ Since {{ alert.start_time | happytime }} + Last Seen {{ alert.last_time | happytime }} {% if alert.end_time %} - Ended {{ alert.end_time }} + Ended {{ alert.end_time | happytime }} {% endif %}
-
+
Module: {{ alert.module }} {% autoescape false %}

{{ alert.description | replace("\n\n", "

") | replace("\n", "
")}}

diff --git a/web_server.py b/web_server.py index 08252a7..b0187a2 100644 --- a/web_server.py +++ b/web_server.py @@ -28,6 +28,7 @@ from helpers.normalisation import get_normalised_filename_if_available from helpers.myradio_api import MyRadioAPI from helpers.alert_manager import AlertManager import package +from baps_types.happytime import happytime env = Environment( loader=FileSystemLoader("%s/ui-templates/" % os.path.dirname(__file__)), @@ -95,6 +96,11 @@ def render_template(file, data, status=200): return html(html_content, status=status) +def _filter_happytime(date): + return happytime(date) + +env.filters["happytime"] = _filter_happytime + logger: LoggingManager server_state: StateManager api: MyRadioAPI From a172d03f0efe3254a28d36afb989da9e47f49d98 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 22 Sep 2021 19:49:24 +0100 Subject: [PATCH 13/40] pep8 --- alerts/dummy.py | 26 ++++---- alerts/player.py | 136 ++++++++++++++++++++------------------- alerts/server.py | 100 ++++++++++++++-------------- baps_types/alert.py | 61 +++++++++--------- baps_types/happytime.py | 2 + helpers/alert_manager.py | 120 +++++++++++++++++----------------- web_server.py | 13 ++-- 7 files changed, 237 insertions(+), 221 deletions(-) diff --git a/alerts/dummy.py b/alerts/dummy.py index 92cd898..7dacca3 100644 --- a/alerts/dummy.py +++ b/alerts/dummy.py @@ -2,17 +2,19 @@ from helpers.alert_manager import AlertProvider from package import BETA from baps_types.alert import WARNING, Alert # Dummy alert provider for testing basics like UI without needing to actually cause errors. + + class DummyAlertProvider(AlertProvider): - def get_alerts(self): - if BETA: - return [Alert( - { - "start_time": -1, - "id": "test", - "title": "BAPSicle is in Debug Mode", - "description": "This is a test alert. It will not appear on production builds.", - "module": "Test", - "severity": WARNING - } - )] + def get_alerts(self): + if BETA: + return [Alert( + { + "start_time": -1, + "id": "test", + "title": "BAPSicle is in Debug Mode", + "description": "This is a test alert. It will not appear on production builds.", + "module": "Test", + "severity": WARNING + } + )] diff --git a/alerts/player.py b/alerts/player.py index ff1b437..1c9b70b 100644 --- a/alerts/player.py +++ b/alerts/player.py @@ -7,88 +7,90 @@ from helpers.alert_manager import AlertProvider from baps_types.alert import CRITICAL, WARNING, Alert from baps_types.happytime import happytime -MODULE = "Player" # This should match the log file, so the UI will link to the logs page. +MODULE = "Player" # This should match the log file, so the UI will link to the logs page. + class PlayerAlertProvider(AlertProvider): - _server_state: Dict[str, Any] - _states: List[Optional[Dict[str,Any]]] = [] - _player_count: int + _server_state: Dict[str, Any] + _states: List[Optional[Dict[str, Any]]] = [] + _player_count: int - def __init__(self): - # Player count only changes after server restart, may as well just load this once. - with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: - self._server_state = json.loads(file.read()) + def __init__(self): + # Player count only changes after server restart, may as well just load this once. + with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: + self._server_state = json.loads(file.read()) - self._player_count = int(self._server_state["num_channels"]) - self._states = [None] * self._player_count + self._player_count = int(self._server_state["num_channels"]) + self._states = [None] * self._player_count - # To simplify monitoring (and allow detection of things going super weird), we are going to read from the state file to work out the alerts. - def get_alerts(self): - for channel in range(self._player_count): - with open(resolve_external_file_path("state/Player{}.json".format(channel))) as file: - self._states[channel] = json.loads(file.read()) + # To simplify monitoring (and allow detection of things going super + # weird), we are going to read from the state file to work out the alerts. + def get_alerts(self): + for channel in range(self._player_count): + with open(resolve_external_file_path("state/Player{}.json".format(channel))) as file: + self._states[channel] = json.loads(file.read()) - funcs = [self._channel_count, self._initialised, self._start_time] + funcs = [self._channel_count, self._initialised, self._start_time] - alerts: List[Alert] = [] + alerts: List[Alert] = [] - for func in funcs: - func_alerts = func() - if func_alerts: - alerts.extend(func_alerts) + for func in funcs: + func_alerts = func() + if func_alerts: + alerts.extend(func_alerts) - return alerts + return alerts - def _channel_count(self): - if self._player_count <= 0: - return [Alert({ - "start_time": -1, # Now - "id": "no_channels", - "title": "There are no players configured.", - "description": "The number of channels configured is {}. Please set to at least 1 on the 'Server Config' page.".format(self._player_count), - "module": MODULE+"Handler", - "severity": CRITICAL - })] + def _channel_count(self): + if self._player_count <= 0: + return [Alert({ + "start_time": -1, # Now + "id": "no_channels", + "title": "There are no players configured.", + "description": "The number of channels configured is {}. Please set to at least 1 on the 'Server Config' page.".format(self._player_count), + "module": MODULE+"Handler", + "severity": CRITICAL + })] - def _initialised(self): - alerts: List[Alert] = [] - for channel in range(self._player_count): - if self._states[channel] and not self._states[channel]["initialised"]: - alerts.append(Alert({ - "start_time": -1, # Now - "id": "player_{}_not_initialised".format(channel), - "title": "Player {} is not initialised.".format(channel), - "description": "This typically means the player channel was not able find the configured sound output on the system. Please check the 'Player Config' and Player logs to determine the cause.", - "module": MODULE+str(channel), - "severity": CRITICAL - })) - return alerts + def _initialised(self): + alerts: List[Alert] = [] + for channel in range(self._player_count): + if self._states[channel] and not self._states[channel]["initialised"]: + alerts.append(Alert({ + "start_time": -1, # Now + "id": "player_{}_not_initialised".format(channel), + "title": "Player {} is not initialised.".format(channel), + "description": "This typically means the player channel was not able find the configured sound output on the system. Please check the 'Player Config' and Player logs to determine the cause.", + "module": MODULE+str(channel), + "severity": CRITICAL + })) + return alerts - def _start_time(self): - server_start_time = self._server_state["start_time"] - server_start_time = datetime.fromtimestamp(server_start_time) - delta = timedelta( - seconds=30, - ) + def _start_time(self): + server_start_time = self._server_state["start_time"] + server_start_time = datetime.fromtimestamp(server_start_time) + delta = timedelta( + seconds=30, + ) - alerts: List[Alert] = [] - for channel in range(self._player_count): - start_time = self._states[channel]["start_time"] - start_time = datetime.fromtimestamp(start_time) - if (start_time > server_start_time + delta): - alerts.append(Alert({ - "start_time": -1, - "id": "player_{}_restarted".format(channel), - "title": "Player {} restarted after the server started.".format(channel), - "description": -"""Player {} last restarted at {}, after the server first started at {}, suggesting a failure. + alerts: List[Alert] = [] + for channel in range(self._player_count): + start_time = self._states[channel]["start_time"] + start_time = datetime.fromtimestamp(start_time) + if (start_time > server_start_time + delta): + alerts.append(Alert({ + "start_time": -1, + "id": "player_{}_restarted".format(channel), + "title": "Player {} restarted after the server started.".format(channel), + "description": + """Player {} last restarted at {}, after the server first started at {}, suggesting a failure. This likely means there was an unhandled exception in the player code, causing the server to restart the player. Please check player logs to investigate the cause. Please restart the server to clear this warning.""" - .format(channel, happytime(start_time), happytime(server_start_time)), - "module": MODULE+str(channel), - "severity": WARNING - })) - return alerts + .format(channel, happytime(start_time), happytime(server_start_time)), + "module": MODULE+str(channel), + "severity": WARNING + })) + return alerts diff --git a/alerts/server.py b/alerts/server.py index 375f87d..6a07d06 100644 --- a/alerts/server.py +++ b/alerts/server.py @@ -7,67 +7,69 @@ from helpers.alert_manager import AlertProvider from baps_types.alert import CRITICAL, WARNING, Alert from baps_types.happytime import happytime -MODULE = "BAPSicleServer" # This should match the log file, so the UI will link to the logs page. +MODULE = "BAPSicleServer" # This should match the log file, so the UI will link to the logs page. + class ServerAlertProvider(AlertProvider): - _state: Dict[str, Any] - # To simplify monitoring (and allow detection of things going super weird), we are going to read from the state file to work out the alerts. - def get_alerts(self): - with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: - self._state = json.loads(file.read()) + _state: Dict[str, Any] + # To simplify monitoring (and allow detection of things going super + # weird), we are going to read from the state file to work out the alerts. + def get_alerts(self): + with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: + self._state = json.loads(file.read()) - funcs = [self._api_key, self._start_time] + funcs = [self._api_key, self._start_time] - alerts: List[Alert] = [] + alerts: List[Alert] = [] - for func in funcs: - func_alerts = func() - if func_alerts: - alerts.extend(func_alerts) + for func in funcs: + func_alerts = func() + if func_alerts: + alerts.extend(func_alerts) - return alerts + return alerts - def _api_key(self): - if not self._state["myradio_api_key"]: - return [Alert({ - "start_time": -1, # Now - "id": "api_key_missing", - "title": "MyRadio API Key is not configured.", - "description": "This means you will be unable to load show plans, audio items, or tracklist. Please set one on the 'Server Config' page.", - "module": MODULE, - "severity": CRITICAL - })] + def _api_key(self): + if not self._state["myradio_api_key"]: + return [Alert({ + "start_time": -1, # Now + "id": "api_key_missing", + "title": "MyRadio API Key is not configured.", + "description": "This means you will be unable to load show plans, audio items, or tracklist. Please set one on the 'Server Config' page.", + "module": MODULE, + "severity": CRITICAL + })] - if len(self._state["myradio_api_key"]) < 10: - return [Alert({ - "start_time": -1, - "id": "api_key_missing", - "title": "MyRadio API Key seems incorrect.", - "description": "The API key is less than 10 characters, it's probably not a valid one. If it is valid, it shouldn't be.", - "module": MODULE, - "severity": WARNING - })] + if len(self._state["myradio_api_key"]) < 10: + return [Alert({ + "start_time": -1, + "id": "api_key_missing", + "title": "MyRadio API Key seems incorrect.", + "description": "The API key is less than 10 characters, it's probably not a valid one. If it is valid, it shouldn't be.", + "module": MODULE, + "severity": WARNING + })] - def _start_time(self): - start_time = self._state["start_time"] - start_time = datetime.fromtimestamp(start_time) - delta = timedelta( - days=1, - ) - if (start_time + delta > datetime.now()): - return [Alert({ - "start_time": -1, - "id": "server_restarted", - "title": "BAPSicle restarted recently.", - "description": -"""The BAPSicle server restarted at {}, less than a day ago. + def _start_time(self): + start_time = self._state["start_time"] + start_time = datetime.fromtimestamp(start_time) + delta = timedelta( + days=1, + ) + if (start_time + delta > datetime.now()): + return [Alert({ + "start_time": -1, + "id": "server_restarted", + "title": "BAPSicle restarted recently.", + "description": + """The BAPSicle server restarted at {}, less than a day ago. It may have been automatically restarted by the OS. If this is not expected, please check logs to investigate why BAPSicle restarted/crashed.""" - .format(happytime(start_time)), - "module": MODULE, - "severity": WARNING - })] + .format(happytime(start_time)), + "module": MODULE, + "severity": WARNING + })] diff --git a/baps_types/alert.py b/baps_types/alert.py index f6958fc..05e792a 100644 --- a/baps_types/alert.py +++ b/baps_types/alert.py @@ -4,6 +4,7 @@ from datetime import datetime CRITICAL = "Critical" WARNING = "Warning" + class Alert: start_time: datetime last_time: datetime @@ -14,56 +15,54 @@ class Alert: module: str severity: str - @property def ui_class(self) -> str: - if self.severity == CRITICAL: - return "danger" - if self.severity == WARNING: - return "warning" - return "info" + if self.severity == CRITICAL: + return "danger" + if self.severity == WARNING: + return "warning" + return "info" # This alert has happened again. def reoccured(self): - self.last_time = datetime.now() - self.end_time = None + self.last_time = datetime.now() + self.end_time = None # This alert has finished, just update end time and keep last_time. def cleared(self): - self.end_time = datetime.now() - + self.end_time = datetime.now() @property def __dict__(self): attrs = ["start_time", "last_time", "end_time", "id", "title", "description", "module", "severity"] out = {} for attr in attrs: - out[attr] = self.__getattribute__(attr) + out[attr] = self.__getattribute__(attr) return out - def __init__(self, new_data: Dict[str,Any]): - required_vars = [ - "start_time", # Just in case an alert wants to show starting earlier than it is reported. - "id", - "title", - "description", - "module", - "severity" - ] + def __init__(self, new_data: Dict[str, Any]): + required_vars = [ + "start_time", # Just in case an alert wants to show starting earlier than it is reported. + "id", + "title", + "description", + "module", + "severity" + ] - for key in required_vars: - if key not in new_data.keys(): - raise KeyError("Key {} is missing from data to create Alert.".format(key)) + for key in required_vars: + if key not in new_data.keys(): + raise KeyError("Key {} is missing from data to create Alert.".format(key)) - #if type(new_data[key]) != type(getattr(self,key)): - # raise TypeError("Key {} has type {}, was expecting {}.".format(key, type(new_data[key]), type(getattr(self,key)))) + # if type(new_data[key]) != type(getattr(self,key)): + # raise TypeError("Key {} has type {}, was expecting {}.".format(key, type(new_data[key]), type(getattr(self,key)))) - # Account for if the creator didn't want to set a custom time. - if key == "start_time" and new_data[key] == -1: - new_data[key] = datetime.now() + # Account for if the creator didn't want to set a custom time. + if key == "start_time" and new_data[key] == -1: + new_data[key] = datetime.now() - setattr(self,key,new_data[key]) + setattr(self, key, new_data[key]) - self.last_time = self.start_time - self.end_time = None + self.last_time = self.start_time + self.end_time = None diff --git a/baps_types/happytime.py b/baps_types/happytime.py index 898730e..9fb6426 100644 --- a/baps_types/happytime.py +++ b/baps_types/happytime.py @@ -1,3 +1,5 @@ from datetime import datetime + + def happytime(date: datetime): return date.strftime("%Y-%m-%d %H:%M:%S") diff --git a/helpers/alert_manager.py b/helpers/alert_manager.py index ab9c476..f2ca4fe 100644 --- a/helpers/alert_manager.py +++ b/helpers/alert_manager.py @@ -1,14 +1,15 @@ from typing import Any, List, Optional -#Magic for importing alert providers from alerts directory. +# Magic for importing alert providers from alerts directory. from pkgutil import iter_modules from importlib import import_module -from inspect import getmembers,isclass +from inspect import getmembers, isclass from sys import modules from baps_types.alert import CRITICAL, Alert import alerts + def iter_namespace(ns_pkg): # Specifying the second argument (prefix) to iter_modules makes the # returned name an absolute name instead of a relative one. This allows @@ -19,75 +20,78 @@ def iter_namespace(ns_pkg): class AlertProvider(): - def __init__(self): - return None + def __init__(self): + return None + + def get_alerts(self): + return [] - def get_alerts(self): - return [] class AlertManager(): - _alerts: List[Alert] - _providers: List[AlertProvider] = [] + _alerts: List[Alert] + _providers: List[AlertProvider] = [] - def __init__(self): - self._alerts = [] + def __init__(self): + self._alerts = [] - # Find all the alert providers from the /alerts/ directory. - providers = { - name: import_module(name) - for _, name, _ - in iter_namespace(alerts) - } + # Find all the alert providers from the /alerts/ directory. + providers = { + name: import_module(name) + for _, name, _ + in iter_namespace(alerts) + } - for provider in providers: - classes: List[Any] = [mem[1] for mem in getmembers(modules[provider], isclass) if mem[1].__module__ == modules[provider].__name__] + for provider in providers: + classes: List[Any] = [ + mem[1] for mem in getmembers( + modules[provider], + isclass) if mem[1].__module__ == modules[provider].__name__] - if (len(classes) != 1): - print(classes) - raise Exception("Can't import plugin " + provider + " because it doesn't have 1 class.") + if (len(classes) != 1): + print(classes) + raise Exception("Can't import plugin " + provider + " because it doesn't have 1 class.") - self._providers.append(classes[0]()) + self._providers.append(classes[0]()) + print("Discovered alert providers: ", self._providers) - print("Discovered alert providers: ", self._providers) + def poll_alerts(self): - def poll_alerts(self): + # Poll modules for any alerts. + new_alerts: List[Optional[Alert]] = [] + for provider in self._providers: + provider_alerts = provider.get_alerts() + if provider_alerts: + new_alerts.extend(provider_alerts) - # Poll modules for any alerts. - new_alerts: List[Optional[Alert]] = [] - for provider in self._providers: - provider_alerts = provider.get_alerts() - if provider_alerts: - new_alerts.extend(provider_alerts) + # Here we replace new firing alerts with older ones, to keep any context. + # (This doesn't do anything yet really, for future use.) + for existing in self._alerts: + found = False + for new in new_alerts: + # given we're removing alerts, got to skip any we removed. + if not new: + continue - # Here we replace new firing alerts with older ones, to keep any context. - # (This doesn't do anything yet really, for future use.) - for existing in self._alerts: - found = False - for new in new_alerts: - # given we're removing alerts, got to skip any we removed. - if not new: - continue + if existing.id == new.id: + # Alert is continuing. Replace it with the old one. + index = new_alerts.index(new) + existing.reoccured() + new_alerts[index] = None # We're going to merge the existing and new, so clear the new one out. + found = True + break + if not found: + # The existing alert is gone, mark it as ended. + existing.cleared() - if existing.id == new.id: - # Alert is continuing. Replace it with the old one. - index = new_alerts.index(new) - existing.reoccured() - new_alerts[index] = None # We're going to merge the existing and new, so clear the new one out. - found = True - break - if found == False: - # The existing alert is gone, mark it as ended. - existing.cleared() + self._alerts.extend([value for value in new_alerts if value]) # Remove any nulled out new alerts - self._alerts.extend([value for value in new_alerts if value]) # Remove any nulled out new alerts + @property + def alerts_current(self): + self.poll_alerts() + return [alert for alert in self._alerts if not alert.end_time] - @property - def alerts_current(self): - self.poll_alerts() - return [alert for alert in self._alerts if not alert.end_time] - - @property - def alerts_previous(self): - self.poll_alerts() - return [alert for alert in self._alerts if alert.end_time] + @property + def alerts_previous(self): + self.poll_alerts() + return [alert for alert in self._alerts if alert.end_time] diff --git a/web_server.py b/web_server.py index b0187a2..1ba3940 100644 --- a/web_server.py +++ b/web_server.py @@ -99,6 +99,7 @@ def render_template(file, data, status=200): def _filter_happytime(date): return happytime(date) + env.filters["happytime"] = _filter_happytime logger: LoggingManager @@ -114,14 +115,17 @@ player_from_q: List[Queue] = [] @app.exception(NotFound) def page_not_found(request, e: Any): - data = {"ui_page": "404", "ui_title": "404", "code": 404, "title": "Page Not Found", "message": "Looks like you fell off the tip of the iceberg." } + data = {"ui_page": "404", "ui_title": "404", "code": 404, "title": "Page Not Found", + "message": "Looks like you fell off the tip of the iceberg."} return render_template("error.html", data=data, status=404) -@app.exception(Exception, ServerError) -def server_error(request, e: Exception): - data = {"ui_page": "500", "ui_title": "500", "code": 500, "title": "Something went very wrong!", "message": "Looks like the server fell over. Try viewing the WebServer logs for more details." } +# Future use. +def error_page(code=500, ui_title="500", title="Something went very wrong!", + message="Looks like the server fell over. Try viewing the WebServer logs for more details."): + data = {"ui_page": ui_title, "ui_title": ui_title, "code": code, "title": title, "message": message} return render_template("error.html", data=data, status=500) + @app.route("/") def ui_index(request): config = server_state.get() @@ -148,6 +152,7 @@ def ui_status(request): "ui_page": "status", "ui_title": "Status"} return render_template("status.html", data=data) + @app.route("/alerts") def ui_alerts(request): data = { From 0cdfd7b2fe2e75ce0b6e2021cb47254f3f8f83e5 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 22 Sep 2021 20:14:58 +0100 Subject: [PATCH 14/40] Fix linting --- alerts/player.py | 7 +++++-- alerts/server.py | 6 ++++-- baps_types/alert.py | 3 ++- helpers/alert_manager.py | 2 +- package.json | 2 +- web_server.py | 3 ++- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/alerts/player.py b/alerts/player.py index 1c9b70b..9ed9da8 100644 --- a/alerts/player.py +++ b/alerts/player.py @@ -48,7 +48,9 @@ class PlayerAlertProvider(AlertProvider): "start_time": -1, # Now "id": "no_channels", "title": "There are no players configured.", - "description": "The number of channels configured is {}. Please set to at least 1 on the 'Server Config' page.".format(self._player_count), + "description": "The number of channels configured is {}. \ + Please set to at least 1 on the 'Server Config' page." + .format(self._player_count), "module": MODULE+"Handler", "severity": CRITICAL })] @@ -61,7 +63,8 @@ class PlayerAlertProvider(AlertProvider): "start_time": -1, # Now "id": "player_{}_not_initialised".format(channel), "title": "Player {} is not initialised.".format(channel), - "description": "This typically means the player channel was not able find the configured sound output on the system. Please check the 'Player Config' and Player logs to determine the cause.", + "description": "This typically means the player channel was not able find the configured sound output \ + on the system. Please check the 'Player Config' and Player logs to determine the cause.", "module": MODULE+str(channel), "severity": CRITICAL })) diff --git a/alerts/server.py b/alerts/server.py index 6a07d06..d89e186 100644 --- a/alerts/server.py +++ b/alerts/server.py @@ -37,7 +37,8 @@ class ServerAlertProvider(AlertProvider): "start_time": -1, # Now "id": "api_key_missing", "title": "MyRadio API Key is not configured.", - "description": "This means you will be unable to load show plans, audio items, or tracklist. Please set one on the 'Server Config' page.", + "description": "This means you will be unable to load show plans, audio items, or tracklist. \ + Please set one on the 'Server Config' page.", "module": MODULE, "severity": CRITICAL })] @@ -47,7 +48,8 @@ class ServerAlertProvider(AlertProvider): "start_time": -1, "id": "api_key_missing", "title": "MyRadio API Key seems incorrect.", - "description": "The API key is less than 10 characters, it's probably not a valid one. If it is valid, it shouldn't be.", + "description": "The API key is less than 10 characters, it's probably not a valid one. \ + If it is valid, it shouldn't be.", "module": MODULE, "severity": WARNING })] diff --git a/baps_types/alert.py b/baps_types/alert.py index 05e792a..33ddd76 100644 --- a/baps_types/alert.py +++ b/baps_types/alert.py @@ -56,7 +56,8 @@ class Alert: raise KeyError("Key {} is missing from data to create Alert.".format(key)) # if type(new_data[key]) != type(getattr(self,key)): - # raise TypeError("Key {} has type {}, was expecting {}.".format(key, type(new_data[key]), type(getattr(self,key)))) + # raise TypeError("Key {} has type {}, was expecting {}." + # .format(key, type(new_data[key]), type(getattr(self,key)))) # Account for if the creator didn't want to set a custom time. if key == "start_time" and new_data[key] == -1: diff --git a/helpers/alert_manager.py b/helpers/alert_manager.py index f2ca4fe..53cdaea 100644 --- a/helpers/alert_manager.py +++ b/helpers/alert_manager.py @@ -6,7 +6,7 @@ from importlib import import_module from inspect import getmembers, isclass from sys import modules -from baps_types.alert import CRITICAL, Alert +from baps_types.alert import Alert import alerts diff --git a/package.json b/package.json index f4fb81f..cae6ea8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "presenter-make": "npm run presenter-install && (rm -r presenter-build || true) && cd presenter && yarn build-baps && cp -r build ../presenter-build && cd ../ && npm install", "test": "echo \"Error: no test specified\" && exit 1", "presenter-start": "cd presenter && yarn start-baps", - "lint": "autopep8 -r -a -a --ignore E402,E226,E24,W50,W690 --max-line-length 127 --in-place --exclude=\"*node_modules*,*venv/*,presenter/*\" . " + "lint": "./venv/bin/autopep8 -r -a -a --ignore E402,E226,E24,W50,W690 --max-line-length 127 --in-place --exclude=\"*node_modules*,*venv/*,presenter/*\" . && ./venv/bin/flake8 . --exclude=\"*node_modules*,*venv/*,presenter/*\" --count --ignore=E402,E226,E24,W50,W690 --max-complexity=25 --max-line-length=127 --statistics" }, "repository": { "type": "git", diff --git a/web_server.py b/web_server.py index 1ba3940..142db8a 100644 --- a/web_server.py +++ b/web_server.py @@ -1,5 +1,5 @@ from sanic import Sanic -from sanic.exceptions import NotFound, ServerError, abort +from sanic.exceptions import NotFound, abort from sanic.response import html, file, redirect from sanic.response import json as resp_json from sanic_cors import CORS @@ -119,6 +119,7 @@ def page_not_found(request, e: Any): "message": "Looks like you fell off the tip of the iceberg."} return render_template("error.html", data=data, status=404) + # Future use. def error_page(code=500, ui_title="500", title="Something went very wrong!", message="Looks like the server fell over. Try viewing the WebServer logs for more details."): From fe562ac60f50e51b612dce32cf9a83e0ef63357a Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 22 Sep 2021 20:31:28 +0100 Subject: [PATCH 15/40] Correct comment --- helpers/alert_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/alert_manager.py b/helpers/alert_manager.py index 53cdaea..b2e2775 100644 --- a/helpers/alert_manager.py +++ b/helpers/alert_manager.py @@ -65,7 +65,7 @@ class AlertManager(): new_alerts.extend(provider_alerts) # Here we replace new firing alerts with older ones, to keep any context. - # (This doesn't do anything yet really, for future use.) + # (This doesn't do much yet really, just remembering the start_time) for existing in self._alerts: found = False for new in new_alerts: From 212f449e13ec81382fb11fdca2961ac576d0d252 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 18:51:30 +0100 Subject: [PATCH 16/40] Show loaded status --- ui-templates/status.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-templates/status.html b/ui-templates/status.html index 17482b5..5b9fc68 100644 --- a/ui-templates/status.html +++ b/ui-templates/status.html @@ -12,6 +12,7 @@

Player {{player.channel}}

Initialised: {{player.initialised}}
+ Successful Load: {{player.loaded}}
Fader Live: {{player.live}}
Current Tracklist: {{player.tracklist_id}}

From 2b2e77fb2798143d3742e2645793d3cf580fcb5e Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 19:25:23 +0100 Subject: [PATCH 17/40] Fix borked API, makes webstudio not white screen. --- helpers/myradio_api.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/helpers/myradio_api.py b/helpers/myradio_api.py index 2744038..0573ba1 100644 --- a/helpers/myradio_api.py +++ b/helpers/myradio_api.py @@ -102,16 +102,21 @@ class MyRadioAPI: self._log("Requesting API V2 URL with method {}: {}".format(method, url)) request = None - if method == "GET": - request = await self.async_call(url, method="GET", timeout=timeout) - elif method == "POST": - self._log("POST data: {}".format(data)) - request = await self.async_call(url, data=data, method="POST", timeout=timeout) - elif method == "PUT": - request = await self.async_call(url, method="PUT", timeout=timeout) - else: - self._logException("Invalid API method. Request not sent.") + try: + if method == "GET": + request = await self.async_call(url, method="GET", timeout=timeout) + elif method == "POST": + self._log("POST data: {}".format(data)) + request = await self.async_call(url, data=data, method="POST", timeout=timeout) + elif method == "PUT": + request = await self.async_call(url, method="PUT", timeout=timeout) + else: + self._logException("Invalid API method. Request not sent.") + return None + except aiohttp.ClientError: + self._logException("Failed async API request.") return None + self._log("Finished request.") return request @@ -291,7 +296,7 @@ class MyRadioAPI: if not request or not isinstance(request, bytes): self._logException("Failed to retrieve music playlists.") - return None + return [] return json.loads(request)["payload"] @@ -302,7 +307,7 @@ class MyRadioAPI: if not request or not isinstance(request, bytes): self._logException("Failed to retrieve music playlists.") - return None + return [] return json.loads(request)["payload"] @@ -319,7 +324,7 @@ class MyRadioAPI: self._logException( "Failed to retrieve items for aux playlist {}.".format(library_id) ) - return None + return [] return json.loads(request)["payload"] @@ -333,7 +338,7 @@ class MyRadioAPI: self._logException( "Failed to retrieve items for music playlist {}.".format(library_id) ) - return None + return [] return json.loads(request)["payload"] @@ -347,7 +352,7 @@ class MyRadioAPI: if not request or not isinstance(request, bytes): self._logException("Failed to search for track.") - return None + return [] return json.loads(request)["payload"] From 1b6b3aa9c88228b7a25f9562154ba1fbecc55ec3 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 19:26:04 +0100 Subject: [PATCH 18/40] 404 correctly on missing audio files. --- web_server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web_server.py b/web_server.py index 142db8a..a66e147 100644 --- a/web_server.py +++ b/web_server.py @@ -426,7 +426,12 @@ async def audio_file(request, type: str, id: int): filename = get_normalised_filename_if_available(filename) # Send file or 404 - return await file(filename) + try: + response = await file(filename) + except FileNotFoundError: + abort(404) + return + return response # Static Files From 49590dcc6c396c9e648001b78d99cc2abfe62c35 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 19:26:37 +0100 Subject: [PATCH 19/40] Fix a failed download leaving temp file. --- helpers/myradio_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpers/myradio_api.py b/helpers/myradio_api.py index 0573ba1..f7bc5d0 100644 --- a/helpers/myradio_api.py +++ b/helpers/myradio_api.py @@ -276,6 +276,8 @@ class MyRadioAPI: request = await self.async_api_call(url, api_version="non") if not request or not isinstance(request, (bytes, bytearray)): + # Remove the .downloading temp file given we gave up trying to download. + os.remove(filename + dl_suffix) return (None, False) if did_download else None try: From 6cfded26cce195bc539dfd7441fdebc0ace78602 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 19:28:13 +0100 Subject: [PATCH 20/40] Semi-fix load failed error in webstudio not showing If a load failed immediately, webstudio didn't see anything loaded, so doesn't report it failed. --- player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/player.py b/player.py index 7e3411e..c88035c 100644 --- a/player.py +++ b/player.py @@ -437,6 +437,9 @@ class Player: "Failed to find weight: {}".format(weight)) return False + # This item exists, so we're comitting to load this item. + self.state.update("loaded_item", loaded_item) + # The file_manager helper may have pre-downloaded the file already, or we've played it before. reload = False if loaded_item.filename == "" or loaded_item.filename is None: @@ -463,10 +466,8 @@ class Player: loaded_item.filename ) - # We're comitting to load this item. + # Given we've just messed around with filenames etc, update the item again. self.state.update("loaded_item", loaded_item) - - # Given we've just messed around with filenames etc, update the item in the show plan too. for i in range(len(showplan)): if showplan[i].weight == weight: self.state.update("show_plan", index=i, value=loaded_item) From 3bf05b2783ed7f850441f2882915edfe932413a4 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 20:06:09 +0100 Subject: [PATCH 21/40] Fix preloader not preloading due to GET_PLAN change to GETPLAN --- player_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player_handler.py b/player_handler.py index da17b64..4cc7ea7 100644 --- a/player_handler.py +++ b/player_handler.py @@ -30,7 +30,7 @@ class PlayerHandler: command = message.split(":")[1] # Let the file manager manage the files based on status and loading new show plan triggers. - if command == "GET_PLAN" or command == "STATUS": + if command == "GETPLAN" or command == "STATUS": file_to_q[channel].put(message) # TODO ENUM From 8cc342b032a3727e88c470d76a1cfa4676482b55 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 25 Sep 2021 20:06:51 +0100 Subject: [PATCH 22/40] 3.1.0 again? --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 939f4ff..addf7be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { "name": "bapsicle", - "version": "3.0.0", + "version": "3.1.0", "lockfileVersion": 1 } From 9dc09905142ea4fd548920695e2fb3eef0960167 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 30 Sep 2021 19:12:26 +0100 Subject: [PATCH 23/40] Add normalisation switch option. --- file_manager.py | 12 ++++++++++++ server.py | 1 + ui-templates/config_server.html | 11 +++++++++++ web_server.py | 2 ++ 4 files changed, 26 insertions(+) diff --git a/file_manager.py b/file_manager.py index 06af766..df8ea47 100644 --- a/file_manager.py +++ b/file_manager.py @@ -29,6 +29,14 @@ class FileManager: current_process().name = process_title terminator = Terminator() + + self.normalisation_mode = server_config.get()["normalisation_mode"] + + if self.normalisation_mode != "on": + self.logger.log.info("Normalisation is disabled.") + else: + self.logger.log.info("Normalisation is enabled.") + self.channel_count = len(channel_from_q) self.channel_received = None self.last_known_show_plan = [[]] * self.channel_count @@ -211,6 +219,10 @@ class FileManager: # If we've preloaded everything, get to work normalising tracks before playback. def do_normalise(self): + + if self.normalisation_mode != "on": + return False + # Some channels still have files to preload, do nothing. if self.known_channels_preloaded != [True] * self.channel_count: return False # Didn't normalise diff --git a/server.py b/server.py index 82dde6e..aa08889 100644 --- a/server.py +++ b/server.py @@ -71,6 +71,7 @@ class BAPSicleServer: "myradio_api_tracklist_source": "", "running_state": "running", "tracklist_mode": "off", + "normalisation_mode": "on", } player_to_q: List[Queue] = [] diff --git a/ui-templates/config_server.html b/ui-templates/config_server.html index 6c9ee7f..20fd45d 100644 --- a/ui-templates/config_server.html +++ b/ui-templates/config_server.html @@ -48,6 +48,17 @@ Delayed tracklisting is 20s, to account for cueing with fader down.
Fader Live means if a BAPS Controller is present with support, tracklists will trigger only if fader is up.

+
+ + +

+ Normalisation requests significant CPU requirements, if you're finding the CPU usuage is too high / causing audio glitches, disable this feature. +


diff --git a/web_server.py b/web_server.py index 142db8a..ee80e33 100644 --- a/web_server.py +++ b/web_server.py @@ -190,6 +190,7 @@ def ui_config_server(request): "state": server_state.get(), "ser_ports": DeviceManager.getSerialPorts(), "tracklist_modes": ["off", "on", "delayed", "fader-live"], + "normalisation_modes": ["off", "on"], } return render_template("config_server.html", data=data) @@ -221,6 +222,7 @@ def ui_config_server_update(request): "myradio_api_tracklist_source") ) server_state.update("tracklist_mode", request.form.get("tracklist_mode")) + server_state.update("normalisation_mode", request.form.get("normalisation_mode")) return redirect("/restart") From 1ee542ea3ec06739a027dc27f7ef8f88373fd906 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 12 Oct 2021 20:45:29 +0100 Subject: [PATCH 24/40] Fix linux to use pulseaudio. --- .gitignore | 2 ++ dev/scripts/get_linux_outputs.py | 24 ++++++++++++++++++++++++ helpers/device_manager.py | 14 ++++++++------ package-lock.json | 12 ++++++++++-- package.json | 5 ++++- player.py | 5 ++++- server.py | 6 ++++-- web_server.py | 15 +++++++++------ 8 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 dev/scripts/get_linux_outputs.py diff --git a/.gitignore b/.gitignore index 66518ac..772a7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ music-tmp/ presenter-build + +node_modules/ \ No newline at end of file diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py new file mode 100644 index 0000000..323b6f2 --- /dev/null +++ b/dev/scripts/get_linux_outputs.py @@ -0,0 +1,24 @@ +import os + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +import pygame._sdl2 as sdl2 +import pygame +from pygame import mixer +#pygame.init() +import time +mixer.init(44100, -16, 2, 1024) +is_capture = 0 # zero to request playback devices, non-zero to request recording devices +num = sdl2.get_num_audio_devices(is_capture) +names = [str(sdl2.get_audio_device_name(i, is_capture), encoding="utf-8") for i in range(num)] +mixer.quit() +for i in names: + print(i) + mixer.init(44100, -16, 2, 1024, devicename=i) + print(mixer.get_init()) + mixer.music.load("/home/mstratford/Downloads/managed_play.mp3") + mixer.music.play() + #my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") + #my_song.play() + time.sleep(5) + pygame.quit() \ No newline at end of file diff --git a/helpers/device_manager.py b/helpers/device_manager.py index ae5c930..a7871f9 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -20,10 +20,10 @@ class DeviceManager: return host_api @classmethod - def _getAudioDevices(cls) -> sd.DeviceList: + def _getSDAudioDevices(cls): # To update the list of devices - # Sadly this doesn't work on MacOS. - if not isMacOS(): + # Sadly this only works on Windows. Linux hangs, MacOS crashes. + if isWindows(): sd._terminate() sd._initialize() devices: sd.DeviceList = sd.query_devices() @@ -31,11 +31,13 @@ class DeviceManager: @classmethod def getAudioOutputs(cls) -> Tuple[List[Dict]]: - host_apis = sd.query_hostapis() - devices: sd.DeviceList = cls._getAudioDevices() + + host_apis = list(sd.query_hostapis()) + devices: sd.DeviceList = cls._getSDAudioDevices() for host_api_id in range(len(host_apis)): - if isWindows() and host_apis[host_api_id]["name"] not in WINDOWS_APIS: + # Linux SDL uses PortAudio, which SoundDevice doesn't find. So mark all as unsable. + if (isWindows() and host_apis[host_api_id]["name"] not in WINDOWS_APIS) or (isLinux()): host_apis[host_api_id]["usable"] = False else: host_apis[host_api_id]["usable"] = True diff --git a/package-lock.json b/package-lock.json index 939f4ff..ac7f15d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,13 @@ { "name": "bapsicle", - "version": "3.0.0", - "lockfileVersion": 1 + "version": "3.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "yarn": { + "version": "1.22.15", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.15.tgz", + "integrity": "sha512-AzoEDxj256BOS/jqDXA3pjyhmi4FRBBUMgYoTHI4EIt2EhREkvH0soPVEtnD+DQIJfU5R9bKhcZ1H9l8zPWeoA==" + } + } } diff --git a/package.json b/package.json index cae6ea8..2d3161e 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "bugs": { "url": "https://github.com/universityradioyork/bapsicle/issues" }, - "homepage": "https://github.com/universityradioyork/bapsicle#readme" + "homepage": "https://github.com/universityradioyork/bapsicle#readme", + "dependencies": { + "yarn": "^1.22.15" + } } diff --git a/player.py b/player.py index 5230432..0574168 100644 --- a/player.py +++ b/player.py @@ -21,8 +21,11 @@ # Stop the Pygame Hello message. import os - os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +from helpers.os_environment import isLinux +# It's the only one we could get to work. +if isLinux(): + os.putenv('SDL_AUDIODRIVER', 'pulseaudio') from queue import Empty import multiprocessing diff --git a/server.py b/server.py index 82dde6e..4734dd4 100644 --- a/server.py +++ b/server.py @@ -23,7 +23,7 @@ import json from setproctitle import setproctitle import psutil -from helpers.os_environment import isMacOS +from helpers.os_environment import isLinux, isMacOS if not isMacOS(): # Rip, this doesn't like threading on MacOS. @@ -206,7 +206,9 @@ class BAPSicleServer: time.sleep(1) def startServer(self): - if isMacOS(): + # On MacOS, the default causes something to keep creating new processes. + # On Linux, this is needed to make pulseaudio initiate properly. + if isMacOS() or isLinux(): multiprocessing.set_start_method("spawn", True) process_title = "startServer" diff --git a/web_server.py b/web_server.py index 142db8a..1ae5068 100644 --- a/web_server.py +++ b/web_server.py @@ -540,9 +540,12 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana ) except Exception: break - loop = asyncio.get_event_loop() - if loop: - loop.close() - if app: - app.stop() - del app + try: + loop = asyncio.get_event_loop() + if loop: + loop.close() + if app: + app.stop() + del app + except: + pass From e4cc6f7b61d8df6bcc79ecf41a25419323baa3b2 Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Wed, 20 Oct 2021 20:49:19 +0100 Subject: [PATCH 25/40] Prompt before restarting the server if anything is playing --- ui-templates/restart-confirm.html | 9 +++++++++ web_server.py | 5 +++++ 2 files changed, 14 insertions(+) create mode 100644 ui-templates/restart-confirm.html diff --git a/ui-templates/restart-confirm.html b/ui-templates/restart-confirm.html new file mode 100644 index 0000000..3f7b3c6 --- /dev/null +++ b/ui-templates/restart-confirm.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content_inner %} +
+

Hang on a second!

+

Something is currently playing. Restarting will interrupt it! Are you sure?

+ Cancel + Confirm +
+{% endblock %} diff --git a/web_server.py b/web_server.py index 142db8a..04ed686 100644 --- a/web_server.py +++ b/web_server.py @@ -496,6 +496,11 @@ def quit(request): @app.route("/restart") def restart(request): + if request.args.get("confirm", '') != "true": + for i in range(server_state.get()["num_channels"]): + state = status(i) + if state["playing"]: + return render_template("restart-confirm.html", data=None) server_state.update("running_state", "restarting") data = { From ecbfaa4a62955bef69b5818f9c352a36960bfa5e Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:46:41 +0000 Subject: [PATCH 26/40] use sudo to install audio pkg --- build/build-linux.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/build-linux.sh b/build/build-linux.sh index c67ee8d..9b27b8a 100755 --- a/build/build-linux.sh +++ b/build/build-linux.sh @@ -7,7 +7,7 @@ build_branch="$(git branch --show-current)" echo "BUILD: str = \"$build_commit\"" > ../build.py echo "BRANCH: str = \"$build_branch\"" >> ../build.py -apt install libportaudio2 +sudo apt install libportaudio2 python3 -m venv ../venv source ../venv/bin/activate @@ -19,6 +19,8 @@ pip3 install -e ../ python3 ./generate-build-exe-config.py +chmod +x output/BAPSicle + python3 ./build-exe.py bash ./build-exe-pyinstaller-command.sh From 592cf11a79141bedf015491214bbd6655c430263 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:47:24 +0000 Subject: [PATCH 27/40] Fix mp3 support on linux with pygame 2.0.1 --- build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/requirements.txt b/build/requirements.txt index 84c18be..015bd70 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,5 +1,5 @@ wheel -pygame==2.0.1 +pygame==2.0.2 sanic==21.3.4 sanic-Cors==1.0.0 syncer==1.3.0 From c475dbb5d5d72e87db75885a76aea4ff8e24de65 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:48:52 +0000 Subject: [PATCH 28/40] Switch to SDL outputs with pulseaudio for linux --- dev/scripts/get_linux_outputs.py | 2 +- helpers/device_manager.py | 15 +++++++++++++++ ui-templates/config_player.html | 21 +++++++++++++++++++++ web_server.py | 8 +++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py index 323b6f2..6449302 100644 --- a/dev/scripts/get_linux_outputs.py +++ b/dev/scripts/get_linux_outputs.py @@ -5,7 +5,7 @@ os.putenv('SDL_AUDIODRIVER', 'pulseaudio') import pygame._sdl2 as sdl2 import pygame from pygame import mixer -#pygame.init() +pygame.init() import time mixer.init(44100, -16, 2, 1024) is_capture = 0 # zero to request playback devices, non-zero to request recording devices diff --git a/helpers/device_manager.py b/helpers/device_manager.py index a7871f9..aa5a7b0 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -1,6 +1,12 @@ from typing import Any, Dict, List, Optional, Tuple import sounddevice as sd from helpers.os_environment import isLinux, isMacOS, isWindows +import os + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +import pygame._sdl2 as sdl2 +from pygame import mixer import glob if isWindows(): @@ -53,6 +59,15 @@ class DeviceManager: return host_apis + @classmethod + def getAudioDevices(cls) -> List[str]: + mixer.init(44100, -16, 2, 1024) + is_capture = 0 # zero to request playback devices, non-zero to request recording devices + num = sdl2.get_num_audio_devices(is_capture) + names = [str(sdl2.get_audio_device_name(i, is_capture), encoding="utf-8") for i in range(num)] + mixer.quit() + return names + @classmethod def getSerialPorts(cls) -> List[Optional[str]]: """Lists serial port names diff --git a/ui-templates/config_player.html b/ui-templates/config_player.html index 78d2c79..6ddbfd3 100644 --- a/ui-templates/config_player.html +++ b/ui-templates/config_player.html @@ -33,6 +33,26 @@ Set for: Default Audio Output

+{% if data.sdl_direct %} +Linux (Pulse Audio) +
+ +{% for output in data.outputs %} +Set for: + {% for channel in data.channels %} + {% if not channel %} + Player {{loop.index0}} + {% elif channel.output == output %} + Player {{channel.channel}} + {% else %} + Player {{channel.channel}} + {% endif %} + / + {% endfor %} +{% if output %}{{output}}{% else %}System Default Output{% endif %}
+{% endfor %} +
+{% else %} {% for host_api in data.outputs %} {{host_api.name}}
@@ -54,4 +74,5 @@ Default Audio Output {% endfor %} {% endfor %} +{% endif %} {% endblock %} diff --git a/web_server.py b/web_server.py index 1ae5068..5b809ef 100644 --- a/web_server.py +++ b/web_server.py @@ -17,6 +17,7 @@ import json import os from helpers.os_environment import ( + isLinux, resolve_external_file_path, resolve_local_file_path, ) @@ -171,11 +172,16 @@ def ui_config_player(request): for i in range(server_state.get()["num_channels"]): channel_states.append(status(i)) - outputs = DeviceManager.getAudioOutputs() + outputs = None + if isLinux(): + outputs = DeviceManager.getAudioDevices() + else: + outputs = DeviceManager.getAudioOutputs() data = { "channels": channel_states, "outputs": outputs, + "sdl_direct": isLinux(), "ui_page": "config", "ui_title": "Player Config", } From 65944e59b396bd0dd5a2cd50cd4494dfdaeb49da Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 23:02:09 +0000 Subject: [PATCH 29/40] Fix lints. --- dev/scripts/get_linux_outputs.py | 6 +++--- web_server.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py index 6449302..1c53024 100644 --- a/dev/scripts/get_linux_outputs.py +++ b/dev/scripts/get_linux_outputs.py @@ -18,7 +18,7 @@ for i in names: print(mixer.get_init()) mixer.music.load("/home/mstratford/Downloads/managed_play.mp3") mixer.music.play() - #my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") - #my_song.play() + # my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") + # my_song.play() time.sleep(5) - pygame.quit() \ No newline at end of file + pygame.quit() diff --git a/web_server.py b/web_server.py index 5b809ef..83acc14 100644 --- a/web_server.py +++ b/web_server.py @@ -181,7 +181,7 @@ def ui_config_player(request): data = { "channels": channel_states, "outputs": outputs, - "sdl_direct": isLinux(), + "sdl_direct": isLinux(), "ui_page": "config", "ui_title": "Player Config", } @@ -553,5 +553,5 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana if app: app.stop() del app - except: + except Exception: pass From 0f25345a6a10bdd925263ed008c514f8c2b509bb Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 23:26:26 +0000 Subject: [PATCH 30/40] Fix pulseaudio error on !linux --- helpers/device_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/device_manager.py b/helpers/device_manager.py index aa5a7b0..df68409 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -4,7 +4,8 @@ from helpers.os_environment import isLinux, isMacOS, isWindows import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" -os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +if isLinux(): + os.putenv('SDL_AUDIODRIVER', 'pulseaudio') import pygame._sdl2 as sdl2 from pygame import mixer import glob From e287e65cf37a6424488269dccc2635cb4beee6b3 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 3 Nov 2021 00:01:40 +0000 Subject: [PATCH 31/40] Default normalisation off due to ffmpeg requirement. --- server.py | 2 +- ui-templates/config_server.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index aa08889..19e3bf1 100644 --- a/server.py +++ b/server.py @@ -71,7 +71,7 @@ class BAPSicleServer: "myradio_api_tracklist_source": "", "running_state": "running", "tracklist_mode": "off", - "normalisation_mode": "on", + "normalisation_mode": "off", } player_to_q: List[Queue] = [] diff --git a/ui-templates/config_server.html b/ui-templates/config_server.html index 20fd45d..18a2852 100644 --- a/ui-templates/config_server.html +++ b/ui-templates/config_server.html @@ -48,7 +48,6 @@ Delayed tracklisting is 20s, to account for cueing with fader down.
Fader Live means if a BAPS Controller is present with support, tracklists will trigger only if fader is up.

-

- Normalisation requests significant CPU requirements, if you're finding the CPU usuage is too high / causing audio glitches, disable this feature. + Normalisation requests significant CPU requirements, if you're finding the CPU usuage is too high / causing audio glitches, disable this feature. ffmpeg or avconf required.


From 71689ce84c2e03d7f15a7a680f1105ebdbd5c65d Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Wed, 3 Nov 2021 00:17:47 +0000 Subject: [PATCH 32/40] Update presenter webstudio to fix node production builds. --- presenter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presenter b/presenter index bddd49f..7cfd48b 160000 --- a/presenter +++ b/presenter @@ -1 +1 @@ -Subproject commit bddd49f4f7bb8ed5f89a967b5ac8c6d3cb04f059 +Subproject commit 7cfd48b003cfac45737b78c084ae45431a207765 From b7f75dfe321f90871d017c20d97aa57d443a04b4 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 6 Dec 2021 22:19:07 +0000 Subject: [PATCH 33/40] Fix marking played / unplayed to all channels from 0. --- presenter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presenter b/presenter index 7cfd48b..238da52 160000 --- a/presenter +++ b/presenter @@ -1 +1 @@ -Subproject commit 7cfd48b003cfac45737b78c084ae45431a207765 +Subproject commit 238da52d94f9a2e1f476e5c8e155e50dd8519cb7 From ac7d7d022246edc73fa72f3c3bbea05a90325ce7 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 17:14:54 +0000 Subject: [PATCH 34/40] merge of "dependabot/pip/build/websockets-9.1" and "dev" --- build/requirements.txt | 4 ++-- websocket_server.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/requirements.txt b/build/requirements.txt index 5fd6c4e..c139e14 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,7 +1,7 @@ wheel pygame==2.0.2 -sanic==21.3.4 -sanic-Cors==1.0.0 +sanic==21.9.3 +sanic-Cors==1.0.1 syncer==1.3.0 aiohttp==3.7.4.post0 mutagen==1.45.1 diff --git a/websocket_server.py b/websocket_server.py index e43ecb3..bff97af 100644 --- a/websocket_server.py +++ b/websocket_server.py @@ -7,7 +7,8 @@ from typing import List import websockets import json from os import _exit -from websockets.server import Serve +from websockets.legacy.server import Serve +from websockets.server import serve from setproctitle import setproctitle from multiprocessing import current_process @@ -39,7 +40,7 @@ class WebsocketServer: self.logger = LoggingManager("Websockets") self.server_name = state.get()["server_name"] - self.websocket_server = websockets.serve( + self.websocket_server = serve( self.websocket_handler, state.get()["host"], state.get()["ws_port"] ) From fd87bbd50604779b79fa0e5b74b920a145588b60 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 17:25:12 +0000 Subject: [PATCH 35/40] Bump to websockets 10.1 --- build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/requirements.txt b/build/requirements.txt index c139e14..ea525e9 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -8,7 +8,7 @@ mutagen==1.45.1 sounddevice==0.4.2 setproctitle==1.2.2 pyttsx3==2.90 -websockets==9.1 +websockets==10.1 typing_extensions==3.10.0.0 pyserial==3.5 requests==2.26.0 From 8b4265a33a62aef1fc143662280fea3d5cf544ee Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 17:43:54 +0000 Subject: [PATCH 36/40] Force node 14 --- .github/workflows/build.yaml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fcd751b..32999fe 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ['3.9'] + node-version: ['14'] steps: - uses: actions/checkout@v2 @@ -17,6 +18,11 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v2 + - name: Set up Node ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} - name: Build .app run: | npm run presenter-make @@ -40,7 +46,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ['3.9'] + node-version: ['14'] steps: - uses: actions/checkout@v2 @@ -48,6 +55,11 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v2 + - name: Set up Node ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} - name: Build executable run: | npm run presenter-make @@ -70,7 +82,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ['3.9'] + node-version: ['14'] steps: - uses: actions/checkout@v2 @@ -78,6 +91,11 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v2 + - name: Set up Node ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} - name: Build .exe run: | npm run presenter-make From ba4e257fdd646b3d9125f9348aad00389af8a545 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 19:31:36 +0000 Subject: [PATCH 37/40] Fix player bootlooping if mixer doesn't init --- player.py | 15 +++++++++++---- websocket_server.py | 22 +++++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/player.py b/player.py index b5455ff..9ce20d2 100644 --- a/player.py +++ b/player.py @@ -22,6 +22,7 @@ # Stop the Pygame Hello message. import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" + from helpers.os_environment import isLinux # It's the only one we could get to work. if isLinux(): @@ -34,7 +35,7 @@ import copy import json import time from typing import Any, Callable, Dict, List, Optional -from pygame import mixer +from pygame import mixer, error from mutagen.mp3 import MP3 from syncer import sync from threading import Timer @@ -160,7 +161,7 @@ class Player: @property def status(self): - state = copy.copy(self.state.state) + state = self.state.state # Not the biggest fan of this, but maybe I'll get a better solution for this later state["loaded_item"] = ( @@ -1054,12 +1055,18 @@ class Player: self.logger.log.info( "Seeking to pos_true: " + str(loaded_state["pos_true"]) ) - self.seek(loaded_state["pos_true"]) + try: + self.seek(loaded_state["pos_true"]) + except error: + self.logger.log.error("Failed to seek on player start. Continuing anyway.") if loaded_state["playing"] is True: self.logger.log.info("Resuming playback on init.") # Use un-pause as we don't want to jump to a new position. - self.unpause() + try: + self.unpause() + except error: + self.logger.log.error("Failed to unpause on player start. Continuing anyway.") else: self.logger.log.info("No file was previously loaded to resume.") diff --git a/websocket_server.py b/websocket_server.py index bff97af..90b1af8 100644 --- a/websocket_server.py +++ b/websocket_server.py @@ -213,7 +213,21 @@ class WebsocketServer: for channel in range(len(self.webstudio_to_q)): try: message = self.webstudio_to_q[channel].get_nowait() - source = message.split(":")[0] + msg_split = message.split(":",3) + parts = len(msg_split) + source = msg_split[0] + command = msg_split[1] + if parts == 4: + #status = msg_split[2] + data = msg_split[3] + elif parts == 3: + data = msg_split[2] + else: + self.logger.log.exception( + "Invalid message size:", msg_split + ) + continue + # TODO ENUM if source not in ["WEBSOCKET", "ALL"]: self.logger.log.error( @@ -223,16 +237,14 @@ class WebsocketServer: ) continue - command = message.split(":")[1] if command == "STATUS": try: - message = message.split("OKAY:")[1] - message = json.loads(message) + message = json.loads(data) except Exception: continue # TODO more logging elif command == "POS": try: - message = message.split(":", 2)[2] + message = data except Exception: continue elif command == "QUIT": From 7a1f7be898e965b5a41c393cdd984fe9c457f3ac Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 20:26:13 +0000 Subject: [PATCH 38/40] Improve efficiency by reducing file test load spam --- player.py | 73 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/player.py b/player.py index 9ce20d2..0020937 100644 --- a/player.py +++ b/player.py @@ -22,7 +22,6 @@ # Stop the Pygame Hello message. import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" - from helpers.os_environment import isLinux # It's the only one we could get to work. if isLinux(): @@ -116,43 +115,45 @@ class Player: @property def isLoaded(self): - return self._isLoaded() + return self.state.get()["loaded"] - def _isLoaded(self, short_test: bool = False): - if not self.state.get()["loaded_item"]: - return False - if self.isPlaying: - return True + def _checkIsLoaded(self, short_test: bool = False): - # If we don't want to do any testing if it's really loaded, fine. - if short_test: - return True + loaded = True + + if not self.state.get()["loaded_item"] or not self.isInit: + loaded = False + elif not self.isPlaying: + # If we don't want to do any testing if it's really loaded, fine. + if not short_test: + + # Because Pygame/SDL is annoying + # We're not playing now, so we can quickly test run + # If that works, we're loaded. + try: + mixer.music.set_volume(0) + mixer.music.play(0) + except Exception: + try: + mixer.music.set_volume(1) + except Exception: + self.logger.log.exception( + "Failed to reset volume after attempting loaded test." + ) + pass + loaded = False + finally: + mixer.music.stop() - # Because Pygame/SDL is annoying - # We're not playing now, so we can quickly test run - # If that works, we're loaded. - try: - mixer.music.set_volume(0) - mixer.music.play(0) - except Exception: - try: mixer.music.set_volume(1) - except Exception: - self.logger.log.exception( - "Failed to reset volume after attempting loaded test." - ) - pass - return False - finally: - mixer.music.stop() - mixer.music.set_volume(1) - return True + self.state.update("loaded", loaded) + return loaded @property def isCued(self): # Don't mess with playback, we only care about if it's supposed to be loaded. - if not self._isLoaded(short_test=True): + if not self.isLoaded: return False return ( self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue @@ -540,6 +541,7 @@ class Player: # Everything worked, we made it! # Write the loaded item again once more, to confirm the filename if we've reattempted. self.state.update("loaded_item", loaded_item) + self._checkIsLoaded() if loaded_item.cue > 0: self.seek(loaded_item.cue) @@ -554,6 +556,7 @@ class Player: # Even though we failed, make sure state is up to date with latest failure. # We're comitting to load this item. self.state.update("loaded_item", loaded_item) + self._checkIsLoaded() return False @@ -901,7 +904,6 @@ class Player: self._ended() self.state.update("playing", self.isPlaying) - self.state.update("loaded", self.isLoaded) self.state.update( "pos_true", @@ -1072,7 +1074,6 @@ class Player: try: while self.running: - time.sleep(0.02) self._updateState() self._ping_times() try: @@ -1098,11 +1099,19 @@ class Player: except Empty: # The incomming message queue was empty, # skip message processing - pass + + # If we're getting no messages, sleep. + # But if we do have messages, once we've done with one, we'll check for the next one more quickly. + time.sleep(0.05) else: # We got a message. + ## Check if we're successfully loaded + # This is here so that we can check often, but not every single loop + # Only when user gives input. + self._checkIsLoaded() + # Output re-inits the mixer, so we can do this any time. if self.last_msg.startswith("OUTPUT"): split = self.last_msg.split(":") From c538ce3a46fb53d6cfc1e30be7013b2624a44202 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Thu, 23 Dec 2021 20:26:37 +0000 Subject: [PATCH 39/40] Fix "STOPPED" message. --- websocket_server.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/websocket_server.py b/websocket_server.py index 90b1af8..413e20b 100644 --- a/websocket_server.py +++ b/websocket_server.py @@ -217,16 +217,12 @@ class WebsocketServer: parts = len(msg_split) source = msg_split[0] command = msg_split[1] + data = None if parts == 4: #status = msg_split[2] data = msg_split[3] elif parts == 3: data = msg_split[2] - else: - self.logger.log.exception( - "Invalid message size:", msg_split - ) - continue # TODO ENUM if source not in ["WEBSOCKET", "ALL"]: From 0b6e245703778c49313741bba03c626c008f7a00 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Fri, 11 Mar 2022 21:58:12 +0000 Subject: [PATCH 40/40] Add ffmpeg for ubuntu --- build/build-linux.sh | 1 + build/install-ubuntu.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build/build-linux.sh b/build/build-linux.sh index 9b27b8a..c5a9ea9 100755 --- a/build/build-linux.sh +++ b/build/build-linux.sh @@ -8,6 +8,7 @@ echo "BUILD: str = \"$build_commit\"" > ../build.py echo "BRANCH: str = \"$build_branch\"" >> ../build.py sudo apt install libportaudio2 +sudo apt install python3-pip python3-venv ffmpeg python3 -m venv ../venv source ../venv/bin/activate diff --git a/build/install-ubuntu.sh b/build/install-ubuntu.sh index fc5ec29..fe92ef4 100755 --- a/build/install-ubuntu.sh +++ b/build/install-ubuntu.sh @@ -1,2 +1,2 @@ #!/bin/bash -sudo apt-get -y install libasound-dev libportaudio2 +sudo apt-get -y install libasound-dev libportaudio2 ffmpeg