Lint with black for formatting.

This commit is contained in:
Matthew Stratford 2021-09-11 16:49:08 +01:00
parent 398c0ac0ce
commit 2941d90f60
16 changed files with 623 additions and 374 deletions

View file

@ -20,8 +20,11 @@ from time import time
from baps_types.marker import Marker from baps_types.marker import Marker
def _time_ms(): def _time_ms():
return round(time() * 1000) return round(time() * 1000)
class PlanItem: class PlanItem:
_timeslotitemid: str = "0" _timeslotitemid: str = "0"
_weight: int = 0 _weight: int = 0
@ -72,7 +75,7 @@ class PlanItem:
self._played_at = _time_ms() self._played_at = _time_ms()
def play_count_decrement(self): def play_count_decrement(self):
self._play_count = max(0,self._play_count - 1) self._play_count = max(0, self._play_count - 1)
if self._play_count == 0: if self._play_count == 0:
self._played_at = 0 self._played_at = 0
@ -118,7 +121,9 @@ class PlanItem:
@property @property
def intro(self) -> float: def intro(self) -> float:
markers = list(filter(lambda m: m.position == "start" and m.section is None, self._markers)) markers = list(
filter(lambda m: m.position == "start" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?) # TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0: if len(markers) > 0:
return markers[0].time return markers[0].time
@ -126,7 +131,9 @@ class PlanItem:
@property @property
def cue(self) -> float: def cue(self) -> float:
markers = list(filter(lambda m: m.position == "mid" and m.section is None, self._markers)) markers = list(
filter(lambda m: m.position == "mid" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?) # TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0: if len(markers) > 0:
return markers[0].time return markers[0].time
@ -134,7 +141,9 @@ class PlanItem:
@property @property
def outro(self) -> float: def outro(self) -> float:
markers = list(filter(lambda m: m.position == "end" and m.section is None, self._markers)) markers = list(
filter(lambda m: m.position == "end" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?) # TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0: if len(markers) > 0:
return markers[0].time return markers[0].time
@ -164,7 +173,7 @@ class PlanItem:
"played": self.play_count > 0, "played": self.play_count > 0,
"played_at": self.played_at, "played_at": self.played_at,
"play_count": self.play_count, "play_count": self.play_count,
"clean": self.clean "clean": self.clean,
} }
def __init__(self, new_item: Dict[str, Any]): def __init__(self, new_item: Dict[str, Any]):
@ -183,40 +192,59 @@ class PlanItem:
self._artist = new_item["artist"] if "artist" in new_item else None self._artist = new_item["artist"] if "artist" in new_item else None
self._length = new_item["length"] self._length = new_item["length"]
self._markers = ( self._markers = (
[Marker(marker) for marker in new_item["markers"]] if "markers" in new_item else [] [Marker(marker) for marker in new_item["markers"]]
if "markers" in new_item
else []
) )
self._play_count = new_item["play_count"] if "play_count" in new_item else 0 self._play_count = new_item["play_count"] if "play_count" in new_item else 0
self._played_at = new_item["played_at"] if "played_at" in new_item else 0 self._played_at = new_item["played_at"] if "played_at" in new_item else 0
self._clean = new_item["clean"] if "clean" in new_item else True self._clean = new_item["clean"] if "clean" in new_item else True
# TODO: Edit this to handle markers when MyRadio supports them # TODO: Edit this to handle markers when MyRadio supports them
if "intro" in new_item and (isinstance(new_item["intro"], int) or isinstance(new_item["intro"], float)) and new_item["intro"] > 0: if (
"intro" in new_item
and (
isinstance(new_item["intro"], int)
or isinstance(new_item["intro"], float)
)
and new_item["intro"] > 0
):
marker = { marker = {
"name": "Intro", "name": "Intro",
"time": new_item["intro"], "time": new_item["intro"],
"position": "start", "position": "start",
"section": None "section": None,
} }
self.set_marker(Marker(json.dumps(marker))) self.set_marker(Marker(json.dumps(marker)))
if "cue" in new_item and (isinstance(new_item["cue"], int) or isinstance(new_item["cue"], float)) and new_item["cue"] > 0: if (
"cue" in new_item
and (isinstance(new_item["cue"], int) or isinstance(new_item["cue"], float))
and new_item["cue"] > 0
):
marker = { marker = {
"name": "Cue", "name": "Cue",
"time": new_item["cue"], "time": new_item["cue"],
"position": "mid", "position": "mid",
"section": None "section": None,
} }
self.set_marker(Marker(json.dumps(marker))) self.set_marker(Marker(json.dumps(marker)))
# TODO: Convert / handle outro being from end of item. # TODO: Convert / handle outro being from end of item.
if "outro" in new_item and (isinstance(new_item["outro"], int) or isinstance(new_item["outro"], float)) and new_item["outro"] > 0: if (
"outro" in new_item
and (
isinstance(new_item["outro"], int)
or isinstance(new_item["outro"], float)
)
and new_item["outro"] > 0
):
marker = { marker = {
"name": "Outro", "name": "Outro",
"time": new_item["outro"], "time": new_item["outro"],
"position": "end", "position": "end",
"section": None "section": None,
} }
self.set_marker(Marker(json.dumps(marker))) self.set_marker(Marker(json.dumps(marker)))
# Fix any OS specific / or \'s # Fix any OS specific / or \'s
if self.filename: if self.filename:
if os.path.sep == "/": if os.path.sep == "/":

View file

@ -1,4 +1,3 @@
from helpers.the_terminator import Terminator from helpers.the_terminator import Terminator
from typing import List, Optional from typing import List, Optional
from multiprocessing import Queue, current_process from multiprocessing import Queue, current_process
@ -95,12 +94,12 @@ class MattchBox(Controller):
self.logger.log.info("Received from controller: " + str(line)) self.logger.log.info("Received from controller: " + str(line))
if line == 255: if line == 255:
self.ser.write(b"\xff") # Send 255 back, this is a keepalive. self.ser.write(b"\xff") # Send 255 back, this is a keepalive.
elif line in [51,52,53]: elif line in [51, 52, 53]:
# We've received a status update about fader live status, fader is down. # We've received a status update about fader live status, fader is down.
self.sendToPlayer(line-51, "SETLIVE:False") self.sendToPlayer(line - 51, "SETLIVE:False")
elif line in [61,62,63]: elif line in [61, 62, 63]:
# We've received a status update about fader live status, fader is up. # We've received a status update about fader live status, fader is up.
self.sendToPlayer(line-61, "SETLIVE:True") self.sendToPlayer(line - 61, "SETLIVE:True")
elif line in [1, 3, 5]: elif line in [1, 3, 5]:
self.sendToPlayer(int(line / 2), "PLAYPAUSE") self.sendToPlayer(int(line / 2), "PLAYPAUSE")
elif line in [2, 4, 6]: elif line in [2, 4, 6]:
@ -136,5 +135,7 @@ class MattchBox(Controller):
self.connect(None) self.connect(None)
def sendToPlayer(self, channel: int, msg: str): def sendToPlayer(self, channel: int, msg: str):
self.logger.log.info("Sending message to player channel {}: {}".format(channel, msg)) self.logger.log.info(
"Sending message to player channel {}: {}".format(channel, msg)
)
self.server_to_q[channel].put("CONTROLLER:" + msg) self.server_to_q[channel].put("CONTROLLER:" + msg)

View file

@ -31,17 +31,20 @@ class FileManager:
terminator = Terminator() terminator = Terminator()
self.channel_count = len(channel_from_q) self.channel_count = len(channel_from_q)
self.channel_received = None self.channel_received = None
self.last_known_show_plan = [[]]*self.channel_count self.last_known_show_plan = [[]] * self.channel_count
self.next_channel_preload = 0 self.next_channel_preload = 0
self.known_channels_preloaded = [False]*self.channel_count self.known_channels_preloaded = [False] * self.channel_count
self.known_channels_normalised = [False]*self.channel_count self.known_channels_normalised = [False] * self.channel_count
self.last_known_item_ids = [[]]*self.channel_count self.last_known_item_ids = [[]] * self.channel_count
try: try:
while not terminator.terminate: while not terminator.terminate:
# If all channels have received the delete command, reset for the next one. # If all channels have received the delete command, reset for the next one.
if (self.channel_received == None or self.channel_received == [True]*self.channel_count): if (
self.channel_received = [False]*self.channel_count self.channel_received == None
or self.channel_received == [True] * self.channel_count
):
self.channel_received = [False] * self.channel_count
for channel in range(self.channel_count): for channel in range(self.channel_count):
try: try:
@ -50,13 +53,16 @@ class FileManager:
continue continue
try: try:
#source = message.split(":")[0] # source = message.split(":")[0]
command = message.split(":",2)[1] command = message.split(":", 2)[1]
# If we have requested a new show plan, empty the music-tmp directory for the previous show. # If we have requested a new show plan, empty the music-tmp directory for the previous show.
if command == "GETPLAN": if command == "GETPLAN":
if self.channel_received != [False]*self.channel_count and self.channel_received[channel] != True: if (
self.channel_received != [False] * self.channel_count
and self.channel_received[channel] != True
):
# We've already received a delete trigger on a channel, let's not delete the folder more than once. # We've already received a delete trigger on a channel, let's not delete the folder more than once.
# If the channel was already in the process of being deleted, the user has requested it again, so allow it. # If the channel was already in the process of being deleted, the user has requested it again, so allow it.
@ -68,28 +74,42 @@ class FileManager:
path: str = resolve_external_file_path("/music-tmp/") path: str = resolve_external_file_path("/music-tmp/")
if not os.path.isdir(path): if not os.path.isdir(path):
self.logger.log.warning("Music-tmp folder is missing, not handling.") self.logger.log.warning(
"Music-tmp folder is missing, not handling."
)
continue continue
files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] files = [
f
for f in os.listdir(path)
if os.path.isfile(os.path.join(path, f))
]
for file in files: for file in files:
if isWindows(): if isWindows():
filepath = path+"\\"+file filepath = path + "\\" + file
else: else:
filepath = path+"/"+file filepath = path + "/" + file
self.logger.log.info("Removing file {} on new show load.".format(filepath)) self.logger.log.info(
"Removing file {} on new show load.".format(
filepath
)
)
try: try:
os.remove(filepath) os.remove(filepath)
except Exception: except Exception:
self.logger.log.warning("Failed to remove, skipping. Likely file is still in use.") self.logger.log.warning(
"Failed to remove, skipping. Likely file is still in use."
)
continue continue
self.channel_received[channel] = True self.channel_received[channel] = True
self.known_channels_preloaded = [False]*self.channel_count self.known_channels_preloaded = [False] * self.channel_count
self.known_channels_normalised = [False]*self.channel_count self.known_channels_normalised = [
False
] * self.channel_count
# If we receive a new status message, let's check for files which have not been pre-loaded. # If we receive a new status message, let's check for files which have not been pre-loaded.
if command == "STATUS": if command == "STATUS":
extra = message.split(":",3) extra = message.split(":", 3)
if extra[2] != "OKAY": if extra[2] != "OKAY":
continue continue
@ -107,28 +127,30 @@ class FileManager:
self.known_channels_preloaded[channel] = False self.known_channels_preloaded[channel] = False
except Exception: except Exception:
self.logger.log.exception("Failed to handle message {} on channel {}.".format(message, channel)) self.logger.log.exception(
"Failed to handle message {} on channel {}.".format(
message, channel
)
)
# Let's try preload / normalise some files now we're free of messages. # Let's try preload / normalise some files now we're free of messages.
preloaded = self.do_preload() preloaded = self.do_preload()
normalised = self.do_normalise() normalised = self.do_normalise()
if (not preloaded and not normalised): if not preloaded and not normalised:
# We didn't do any hard work, let's sleep. # We didn't do any hard work, let's sleep.
sleep(0.2) sleep(0.2)
except Exception as e: except Exception as e:
self.logger.log.exception( self.logger.log.exception("Received unexpected exception: {}".format(e))
"Received unexpected exception: {}".format(e))
del self.logger del self.logger
# Attempt to preload a file onto disk. # Attempt to preload a file onto disk.
def do_preload(self): def do_preload(self):
channel = self.next_channel_preload channel = self.next_channel_preload
# All channels have preloaded all files, do nothing. # All channels have preloaded all files, do nothing.
if (self.known_channels_preloaded == [True]*self.channel_count): if self.known_channels_preloaded == [True] * self.channel_count:
return False # Didn't preload anything return False # Didn't preload anything
# Right, let's have a quick check in the status for shows without filenames, to preload them. # Right, let's have a quick check in the status for shows without filenames, to preload them.
@ -141,10 +163,16 @@ class FileManager:
# We've not downloaded this file yet, let's do that. # We've not downloaded this file yet, let's do that.
if not item_obj.filename: if not item_obj.filename:
self.logger.log.info("Checking pre-load on channel {}, weight {}: {}".format(channel, item_obj.weight, item_obj.name)) self.logger.log.info(
"Checking pre-load on channel {}, weight {}: {}".format(
channel, item_obj.weight, item_obj.name
)
)
# Getting the file name will only pull the new file if the file doesn't already exist, so this is not too inefficient. # Getting the file name will only pull the new file if the file doesn't already exist, so this is not too inefficient.
item_obj.filename,did_download = sync(self.api.get_filename(item_obj, True)) item_obj.filename, did_download = sync(
self.api.get_filename(item_obj, True)
)
# Alright, we've done one, now let's give back control to process new statuses etc. # Alright, we've done one, now let's give back control to process new statuses etc.
# Save back the resulting item back in regular dict form # Save back the resulting item back in regular dict form
@ -152,7 +180,9 @@ class FileManager:
if did_download: if did_download:
downloaded_something = True downloaded_something = True
self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename)) self.logger.log.info(
"File successfully preloaded: {}".format(item_obj.filename)
)
break break
else: else:
# We didn't download anything this time, file was already loaded. # We didn't download anything this time, file was already loaded.
@ -168,15 +198,14 @@ class FileManager:
return downloaded_something return downloaded_something
# If we've preloaded everything, get to work normalising tracks before playback. # If we've preloaded everything, get to work normalising tracks before playback.
def do_normalise(self): def do_normalise(self):
# Some channels still have files to preload, do nothing. # Some channels still have files to preload, do nothing.
if (self.known_channels_preloaded != [True]*self.channel_count): if self.known_channels_preloaded != [True] * self.channel_count:
return False # Didn't normalise return False # Didn't normalise
# Quit early if all channels are normalised already. # Quit early if all channels are normalised already.
if (self.known_channels_normalised == [True]*self.channel_count): if self.known_channels_normalised == [True] * self.channel_count:
return False return False
channel = self.next_channel_preload channel = self.next_channel_preload
@ -189,16 +218,22 @@ class FileManager:
filename = item_obj.filename filename = item_obj.filename
if not filename: if not filename:
self.logger.log.exception("Somehow got empty filename when all channels are preloaded.") self.logger.log.exception(
"Somehow got empty filename when all channels are preloaded."
)
continue # Try next song. continue # Try next song.
elif (not os.path.isfile(filename)): elif not os.path.isfile(filename):
self.logger.log.exception("Filename for normalisation does not exist. This is bad.") self.logger.log.exception(
"Filename for normalisation does not exist. This is bad."
)
continue continue
elif "normalised" in filename: elif "normalised" in filename:
continue continue
# Sweet, we now need to try generating a normalised version. # Sweet, we now need to try generating a normalised version.
try: try:
self.logger.log.info("Normalising on channel {}: {}".format(channel,filename)) self.logger.log.info(
"Normalising on channel {}: {}".format(channel, filename)
)
# This will return immediately if we already have a normalised file. # This will return immediately if we already have a normalised file.
item_obj.filename = generate_normalised_file(filename) item_obj.filename = generate_normalised_file(filename)
# TODO Hacky # TODO Hacky
@ -216,8 +251,3 @@ class FileManager:
self.next_channel_preload = 0 self.next_channel_preload = 0
return normalised_something return normalised_something

View file

@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple
import sounddevice as sd import sounddevice as sd
from helpers.os_environment import isLinux, isMacOS, isWindows from helpers.os_environment import isLinux, isMacOS, isWindows
import glob import glob
if isWindows(): if isWindows():
from serial.tools.list_ports_windows import comports from serial.tools.list_ports_windows import comports
@ -39,7 +40,9 @@ class DeviceManager:
else: else:
host_apis[host_api_id]["usable"] = True host_apis[host_api_id]["usable"] = True
host_api_devices = (device for device in devices if device["hostapi"] == host_api_id) host_api_devices = (
device for device in devices if device["hostapi"] == host_api_id
)
outputs: List[Dict] = list(filter(cls._isOutput, host_api_devices)) outputs: List[Dict] = list(filter(cls._isOutput, host_api_devices))
outputs = sorted(outputs, key=lambda k: k["name"]) outputs = sorted(outputs, key=lambda k: k["name"])

View file

@ -56,7 +56,8 @@ class MyRadioAPI:
async with func as response: async with func as response:
if response.status != status_code: if response.status != status_code:
self._logException( self._logException(
"Failed to get API request. Status code: " + str(response.status) "Failed to get API request. Status code: "
+ str(response.status)
) )
self._logException(str(await response.text())) self._logException(str(await response.text()))
return await response.read() return await response.read()
@ -81,7 +82,9 @@ class MyRadioAPI:
self._logException(str(r.text)) self._logException(str(r.text))
return json.loads(r.text) if json_payload else r.text return json.loads(r.text) if json_payload else r.text
async def async_api_call(self, url, api_version="v2", method="GET", data=None, timeout=10): async def async_api_call(
self, url, api_version="v2", method="GET", data=None, timeout=10
):
if api_version == "v2": if api_version == "v2":
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url) url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
elif api_version == "non": elif api_version == "non":
@ -198,7 +201,6 @@ class MyRadioAPI:
self.logger.log.error("Show plan in unknown format.") self.logger.log.error("Show plan in unknown format.")
return None return None
# Audio Library # Audio Library
async def get_filename(self, item: PlanItem, did_download: bool = False): async def get_filename(self, item: PlanItem, did_download: bool = False):
@ -240,15 +242,18 @@ class MyRadioAPI:
# If something else (another channel, the preloader etc) is downloading the track, wait for it. # If something else (another channel, the preloader etc) is downloading the track, wait for it.
if os.path.isfile(filename + dl_suffix): if os.path.isfile(filename + dl_suffix):
time_waiting_s = 0 time_waiting_s = 0
self._log("Waiting for download to complete from another worker. " + filename, DEBUG) self._log(
"Waiting for download to complete from another worker. " + filename,
DEBUG,
)
while time_waiting_s < 20: while time_waiting_s < 20:
# TODO: Make something better here. # TODO: Make something better here.
# If the connectivity is super poor or we're loading reeaaaalllly long files, this may be annoying, but this is just in case somehow the other api download gives up. # If the connectivity is super poor or we're loading reeaaaalllly long files, this may be annoying, but this is just in case somehow the other api download gives up.
if os.path.isfile(filename): if os.path.isfile(filename):
# Now the file is downloaded successfully # Now the file is downloaded successfully
return (filename, False) if did_download else filename return (filename, False) if did_download else filename
time_waiting_s +=1 time_waiting_s += 1
self._log("Still waiting",DEBUG) self._log("Still waiting", DEBUG)
time.sleep(1) time.sleep(1)
# File doesn't exist, download it. # File doesn't exist, download it.
@ -300,7 +305,7 @@ class MyRadioAPI:
async def get_playlist_aux_items(self, library_id: str): async def get_playlist_aux_items(self, library_id: str):
# Sometimes they have "aux-<ID>", we only need the index. # Sometimes they have "aux-<ID>", we only need the index.
if library_id.index("-") > -1: if library_id.index("-") > -1:
library_id = library_id[library_id.index("-") + 1:] library_id = library_id[library_id.index("-") + 1 :]
url = "/nipswebPlaylist/{}/items".format(library_id) url = "/nipswebPlaylist/{}/items".format(library_id)
request = await self.async_api_call(url) request = await self.async_api_call(url)
@ -351,12 +356,14 @@ class MyRadioAPI:
source: str = self.config.get()["myradio_api_tracklist_source"] source: str = self.config.get()["myradio_api_tracklist_source"]
data = { data = {
"trackid": item.trackid, "trackid": item.trackid,
"sourceid": int(source) if source.isnumeric() else source "sourceid": int(source) if source.isnumeric() else source,
} }
# Starttime and timeslotid are default in the API to current time/show. # Starttime and timeslotid are default in the API to current time/show.
tracklist_id = None tracklist_id = None
try: try:
tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)["payload"]["audiologid"] tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)[
"payload"
]["audiologid"]
except Exception as e: except Exception as e:
self._logException("Failed to get tracklistid. {}".format(e)) self._logException("Failed to get tracklistid. {}".format(e))
@ -370,7 +377,9 @@ class MyRadioAPI:
self._log("Tracklistitemid is None, can't end tracklist.", WARNING) self._log("Tracklistitemid is None, can't end tracklist.", WARNING)
return False return False
if not isinstance(tracklistitemid, int): if not isinstance(tracklistitemid, int):
self._logException("Tracklistitemid '{}' is not an integer!".format(tracklistitemid)) self._logException(
"Tracklistitemid '{}' is not an integer!".format(tracklistitemid)
)
return False return False
self._log("Ending tracklistitemid {}".format(tracklistitemid)) self._log("Ending tracklistitemid {}".format(tracklistitemid))

View file

@ -1,49 +1,44 @@
import os import os
from helpers.os_environment import resolve_external_file_path
from pydub import AudioSegment, effects # Audio leveling! from pydub import AudioSegment, effects # Audio leveling!
# Stuff to help make BAPSicle play out leveled audio. # Stuff to help make BAPSicle play out leveled audio.
def match_target_amplitude(sound, target_dBFS):
change_in_dBFS = target_dBFS - sound.dBFS
return sound.apply_gain(change_in_dBFS)
# Takes filename in, normalialises it and returns a normalised file path. # Takes filename in, normalialises it and returns a normalised file path.
def generate_normalised_file(filename: str): def generate_normalised_file(filename: str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))): if not (isinstance(filename, str) and filename.endswith(".mp3")):
raise ValueError("Invalid filename given.") raise ValueError("Invalid filename given.")
# Already normalised. # Already normalised.
if filename.endswith("-normalised.mp3"): if filename.endswith("-normalised.mp3"):
return filename return filename
normalised_filename = "{}-normalised.mp3".format(filename.rsplit(".",1)[0]) normalised_filename = "{}-normalised.mp3".format(filename.rsplit(".", 1)[0])
# The file already exists, short circuit. # The file already exists, short circuit.
if (os.path.exists(normalised_filename)): if os.path.exists(normalised_filename):
return normalised_filename return normalised_filename
sound = AudioSegment.from_file(filename, "mp3") sound = AudioSegment.from_file(filename, "mp3")
normalised_sound = effects.normalize(sound) #match_target_amplitude(sound, -10) normalised_sound = effects.normalize(sound)
normalised_sound.export(normalised_filename, bitrate="320k", format="mp3") normalised_sound.export(normalised_filename, bitrate="320k", format="mp3")
return normalised_filename return normalised_filename
# Returns either a normalised file path (based on filename), or the original if not available. # Returns either a normalised file path (based on filename), or the original if not available.
def get_normalised_filename_if_available(filename:str): def get_normalised_filename_if_available(filename: str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))): if not (isinstance(filename, str) and filename.endswith(".mp3")):
raise ValueError("Invalid filename given.") raise ValueError("Invalid filename given.")
# Already normalised. # Already normalised.
if filename.endswith("-normalised.mp3"): if filename.endswith("-normalised.mp3"):
return filename return filename
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3")) normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
# normalised version exists # normalised version exists
if (os.path.exists(normalised_filename)): if os.path.exists(normalised_filename):
return normalised_filename return normalised_filename
# Else we've not got a normalised verison, just take original. # Else we've not got a normalised verison, just take original.
return filename return filename

View file

@ -1,6 +1,6 @@
import json import json
import os import os
from logging import CRITICAL, DEBUG, INFO from logging import DEBUG, INFO
import time import time
from datetime import datetime from datetime import datetime
from copy import copy from copy import copy
@ -79,7 +79,7 @@ class StateManager:
# If there are any new config options in the default state, save them. # If there are any new config options in the default state, save them.
# Uses update() to save them to file too. # Uses update() to save them to file too.
for key in default_state.keys(): for key in default_state.keys():
if not key in file_state.keys(): if key not in file_state.keys():
self.update(key, default_state[key]) self.update(key, default_state[key])
except Exception: except Exception:
@ -114,7 +114,6 @@ class StateManager:
now = datetime.now() now = datetime.now()
current_time = now.strftime("%H:%M:%S") current_time = now.strftime("%H:%M:%S")
state_to_json["last_updated"] = current_time state_to_json["last_updated"] = current_time
@ -154,20 +153,30 @@ class StateManager:
allow = False allow = False
# It's hard to compare lists, especially of complex objects like show plans, just write it. # It's hard to compare lists, especially of complex objects like show plans, just write it.
if (isinstance(value, list)): if isinstance(value, list):
allow = True allow = True
# If the two objects have dict representations, and they don't match, allow writing. # If the two objects have dict representations, and they don't match, allow writing.
# TODO: This should be easier. # TODO: This should be easier.
if (getattr(value, "__dict__", None) and getattr(state_to_update[key], "__dict__", None)): if getattr(value, "__dict__", None) and getattr(
state_to_update[key], "__dict__", None
):
if value.__dict__ != state_to_update[key].__dict__: if value.__dict__ != state_to_update[key].__dict__:
allow = True allow = True
if not allow: if not allow:
# Just some debug logging. # Just some debug logging.
if update_file and (key not in ["playing", "loaded", "initialised", "remaining", "pos_true"]): if update_file and (
self._log("Not updating state for key '{}' with value '{}' of type '{}'.".format(key, value, type(value)), DEBUG) key
not in ["playing", "loaded", "initialised", "remaining", "pos_true"]
):
self._log(
"Not updating state for key '{}' with value '{}' of type '{}'.".format(
key, value, type(value)
),
DEBUG,
)
# We're trying to update the state with the same value. # We're trying to update the state with the same value.
# In this case, ignore the update # In this case, ignore the update
@ -176,11 +185,21 @@ class StateManager:
if index > -1 and key in state_to_update: if index > -1 and key in state_to_update:
if not isinstance(state_to_update[key], list): if not isinstance(state_to_update[key], list):
self._log("Not updating state for key '{}' with value '{}' of type '{}' since index is set and key is not a list.".format(key, value, type(value)), DEBUG) self._log(
"Not updating state for key '{}' with value '{}' of type '{}' since index is set and key is not a list.".format(
key, value, type(value)
),
DEBUG,
)
return return
list_items = state_to_update[key] list_items = state_to_update[key]
if index >= len(list_items): if index >= len(list_items):
self._log("Not updating state for key '{}' with value '{}' of type '{}' because index '{}' is too large..".format(key, value, type(value), index), DEBUG) self._log(
"Not updating state for key '{}' with value '{}' of type '{}' because index '{}' is too large..".format(
key, value, type(value), index
),
DEBUG,
)
return return
list_items[index] = value list_items[index] = value
state_to_update[key] = list_items state_to_update[key] = list_items
@ -190,7 +209,12 @@ class StateManager:
self.state = state_to_update self.state = state_to_update
if update_file: if update_file:
self._log("Writing change to key '{}' with value '{}' of type '{}' to disk.".format(key, value, type(value)), DEBUG) self._log(
"Writing change to key '{}' with value '{}' of type '{}' to disk.".format(
key, value, type(value)
),
DEBUG,
)
# Either a routine write, or state has changed. # Either a routine write, or state has changed.
# Update the file # Update the file
self.write_to_file(state_to_update) self.write_to_file(state_to_update)

View file

@ -70,7 +70,11 @@ if __name__ == "__main__":
if sys.argv[1] == "Presenter": if sys.argv[1] == "Presenter":
webbrowser.open("http://localhost:13500/presenter/") webbrowser.open("http://localhost:13500/presenter/")
except Exception as e: except Exception as e:
print("ALERT:BAPSicle failed with exception of type {}:{}".format(type(e).__name__, e)) print(
"ALERT:BAPSicle failed with exception of type {}:{}".format(
type(e).__name__, e
)
)
sys.exit(1) sys.exit(1)
sys.exit(0) sys.exit(0)

View file

@ -15,9 +15,10 @@ with open(resolve_local_file_path("package.json")) as file:
build_beta = True build_beta = True
try: try:
import build import build
build_commit = build.BUILD build_commit = build.BUILD
build_branch = build.BRANCH build_branch = build.BRANCH
build_beta = (build_branch != "release") build_beta = build_branch != "release"
except (ModuleNotFoundError, AttributeError): except (ModuleNotFoundError, AttributeError):
pass pass
BUILD: str = build_commit BUILD: str = build_commit

242
player.py
View file

@ -21,6 +21,7 @@
# Stop the Pygame Hello message. # Stop the Pygame Hello message.
import os import os
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
from queue import Empty from queue import Empty
@ -148,7 +149,10 @@ class Player:
# Don't mess with playback, we only care about if it's supposed to be loaded. # 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(short_test=True):
return False return False
return (self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue and not self.isPlaying) return (
self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue
and not self.isPlaying
)
@property @property
def status(self): def status(self):
@ -251,7 +255,9 @@ class Player:
return False return False
return True return True
else: else:
self.logger.log.debug("Not playing during seek, setting pos state for next play.") self.logger.log.debug(
"Not playing during seek, setting pos state for next play."
)
self.stopped_manually = True # Don't trigger _ended() on seeking. self.stopped_manually = True # Don't trigger _ended() on seeking.
if pos > 0: if pos > 0:
self.state.update("paused", True) self.state.update("paused", True)
@ -298,7 +304,9 @@ class Player:
# Kinda a bodge for the moment, each "Ghost" (item which is not saved in the database showplan yet) needs to have a unique temporary item. # Kinda a bodge for the moment, each "Ghost" (item which is not saved in the database showplan yet) needs to have a unique temporary item.
# To do this, we'll start with the channel number the item was originally added to (to stop items somehow simultaneously added to different channels from having the same id) # To do this, we'll start with the channel number the item was originally added to (to stop items somehow simultaneously added to different channels from having the same id)
# And chuck in the unix epoch in ns for good measure. # And chuck in the unix epoch in ns for good measure.
item.timeslotitemid = "GHOST-{}-{}".format(self.state.get()["channel"], time.time_ns()) item.timeslotitemid = "GHOST-{}-{}".format(
self.state.get()["channel"], time.time_ns()
)
return item return item
# TODO Allow just moving an item inside the channel instead of removing and adding. # TODO Allow just moving an item inside the channel instead of removing and adding.
@ -315,7 +323,6 @@ class Player:
self._fix_and_update_weights(plan_copy) self._fix_and_update_weights(plan_copy)
loaded_item = self.state.get()["loaded_item"] loaded_item = self.state.get()["loaded_item"]
if loaded_item: if loaded_item:
@ -346,13 +353,15 @@ class Player:
def remove_from_plan(self, weight: int) -> bool: def remove_from_plan(self, weight: int) -> bool:
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"]) plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
found: Optional[PlanItem ] = None found: Optional[PlanItem] = None
before = [] before = []
for item in plan_copy: for item in plan_copy:
before += (item.weight, item.name) before += (item.weight, item.name)
self.logger.log.debug("Weights before removing weight {}:\n{}".format(weight, before)) self.logger.log.debug(
"Weights before removing weight {}:\n{}".format(weight, before)
)
for i in plan_copy: for i in plan_copy:
if i.weight == weight: if i.weight == weight:
@ -372,18 +381,16 @@ class Player:
# So we'll want to update the weight. # So we'll want to update the weight.
# We're removing the loaded item from the channel. # We're removing the loaded item from the channel.
#if loaded_item.weight == weight: # if loaded_item.weight == weight:
loaded_item.weight = -1 loaded_item.weight = -1
# If loaded_item wasn't the same instance, we'd want to do the below. # If loaded_item wasn't the same instance, we'd want to do the below.
# We removed an item above it. Shift it up. # We removed an item above it. Shift it up.
#elif loaded_item.weight > weight: # elif loaded_item.weight > weight:
# loaded_item.weight -= 1 # loaded_item.weight -= 1
# Else, new weight stays the same. # Else, new weight stays the same.
#else: # else:
# return True # return True
self.state.update("loaded_item", loaded_item) self.state.update("loaded_item", loaded_item)
@ -399,7 +406,10 @@ class Player:
loaded_state = self.state.get() loaded_state = self.state.get()
self.unload() self.unload()
self.logger.log.info("Resetting output (in case of sound output gone silent somehow) to " + str(loaded_state["output"])) self.logger.log.info(
"Resetting output (in case of sound output gone silent somehow) to "
+ str(loaded_state["output"])
)
self.output(loaded_state["output"]) self.output(loaded_state["output"])
showplan = loaded_state["show_plan"] showplan = loaded_state["show_plan"]
@ -412,14 +422,12 @@ class Player:
break break
if loaded_item is None: if loaded_item is None:
self.logger.log.error( self.logger.log.error("Failed to find weight: {}".format(weight))
"Failed to find weight: {}".format(weight))
return False return False
reload = False reload = False
if loaded_item.filename == "" or loaded_item.filename is None: if loaded_item.filename == "" or loaded_item.filename is None:
self.logger.log.info( self.logger.log.info("Filename is not specified, loading from API.")
"Filename is not specified, loading from API.")
reload = True reload = True
elif not os.path.exists(loaded_item.filename): elif not os.path.exists(loaded_item.filename):
self.logger.log.warn( self.logger.log.warn(
@ -434,7 +442,9 @@ class Player:
return False return False
# Swap with a normalised version if it's ready, else returns original. # Swap with a normalised version if it's ready, else returns original.
loaded_item.filename = get_normalised_filename_if_available(loaded_item.filename) loaded_item.filename = get_normalised_filename_if_available(
loaded_item.filename
)
self.state.update("loaded_item", loaded_item) self.state.update("loaded_item", loaded_item)
@ -452,8 +462,7 @@ class Player:
while load_attempt < 5: while load_attempt < 5:
load_attempt += 1 load_attempt += 1
try: try:
self.logger.log.info("Loading file: " + self.logger.log.info("Loading file: " + str(loaded_item.filename))
str(loaded_item.filename))
mixer.music.load(loaded_item.filename) mixer.music.load(loaded_item.filename)
except Exception: except Exception:
# We couldn't load that file. # We couldn't load that file.
@ -464,7 +473,9 @@ class Player:
continue # Try loading again. continue # Try loading again.
if not self.isLoaded: if not self.isLoaded:
self.logger.log.error("Pygame loaded file without error, but never actually loaded.") self.logger.log.error(
"Pygame loaded file without error, but never actually loaded."
)
time.sleep(1) time.sleep(1)
continue # Try loading again. continue # Try loading again.
@ -475,12 +486,11 @@ class Player:
else: else:
# WARNING! Pygame / SDL can't seek .wav files :/ # WARNING! Pygame / SDL can't seek .wav files :/
self.state.update( self.state.update(
"length", mixer.Sound( "length",
loaded_item.filename).get_length() / 1000 mixer.Sound(loaded_item.filename).get_length() / 1000,
) )
except Exception: except Exception:
self.logger.log.exception( self.logger.log.exception("Failed to update the length of item.")
"Failed to update the length of item.")
time.sleep(1) time.sleep(1)
continue # Try loading again. continue # Try loading again.
@ -561,7 +571,11 @@ class Player:
try: try:
marker = Marker(marker_str) marker = Marker(marker_str)
except Exception as e: except Exception as e:
self.logger.log.error("Failed to create Marker instance with {} {}: {}".format(timeslotitemid, marker_str, e)) self.logger.log.error(
"Failed to create Marker instance with {} {}: {}".format(
timeslotitemid, marker_str, e
)
)
return False return False
if timeslotitemid == "-1": if timeslotitemid == "-1":
@ -569,10 +583,12 @@ class Player:
if not self.isLoaded: if not self.isLoaded:
return False return False
timeslotitemid = self.state.get()["loaded_item"].timeslotitemid timeslotitemid = self.state.get()["loaded_item"].timeslotitemid
elif self.isLoaded and self.state.get()["loaded_item"].timeslotitemid == timeslotitemid: elif (
self.isLoaded
and self.state.get()["loaded_item"].timeslotitemid == timeslotitemid
):
set_loaded = True set_loaded = True
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"]) plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
for i in range(len(self.state.get()["show_plan"])): for i in range(len(self.state.get()["show_plan"])):
@ -585,15 +601,23 @@ class Player:
except Exception as e: except Exception as e:
self.logger.log.error( self.logger.log.error(
"Failed to set marker on item {}: {} with marker \n{}".format(timeslotitemid, e, marker)) "Failed to set marker on item {}: {} with marker \n{}".format(
timeslotitemid, e, marker
)
)
success = False success = False
if set_loaded: if set_loaded:
try: try:
self.state.update("loaded_item", self.state.get()["loaded_item"].set_marker(marker)) self.state.update(
"loaded_item", self.state.get()["loaded_item"].set_marker(marker)
)
except Exception as e: except Exception as e:
self.logger.log.error( self.logger.log.error(
"Failed to set marker on loaded_item {}: {} with marker \n{}".format(timeslotitemid, e, marker)) "Failed to set marker on loaded_item {}: {} with marker \n{}".format(
timeslotitemid, e, marker
)
)
success = False success = False
return success return success
@ -605,7 +629,9 @@ class Player:
item.play_count_increment() if played else item.play_count_reset() item.play_count_increment() if played else item.play_count_reset()
self.state.update("show_plan", plan) self.state.update("show_plan", plan)
elif len(plan) > weight: elif len(plan) > weight:
plan[weight].play_count_increment() if played else plan[weight].play_count_reset() plan[weight].play_count_increment() if played else plan[
weight
].play_count_reset()
self.state.update("show_plan", plan[weight], weight) self.state.update("show_plan", plan[weight], weight)
else: else:
return False return False
@ -617,11 +643,10 @@ class Player:
self.state.update("live", live) self.state.update("live", live)
# If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing. # If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing.
if (live): if live:
self._potentially_tracklist() self._potentially_tracklist()
return True return True
# Helper functions # Helper functions
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading. # This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
@ -629,18 +654,24 @@ class Player:
mode = self.state.get()["tracklist_mode"] mode = self.state.get()["tracklist_mode"]
time: int = -1 time: int = -1
if mode in ["on","fader-live"]: if mode in ["on", "fader-live"]:
time = 1 # Let's do it pretty quickly. time = 1 # Let's do it pretty quickly.
elif mode == "delayed": elif mode == "delayed":
# Let's do it in a bit, once we're sure it's been playing. (Useful if we've got no idea if it's live or cueing.) # Let's do it in a bit, once we're sure it's been playing. (Useful if we've got no idea if it's live or cueing.)
time = TRACKLISTING_DELAYED_S time = TRACKLISTING_DELAYED_S
if time >= 0 and not self.tracklist_start_timer: if time >= 0 and not self.tracklist_start_timer:
self.logger.log.info("Setting timer for tracklisting in {} secs due to Mode: {}".format(time, mode)) self.logger.log.info(
"Setting timer for tracklisting in {} secs due to Mode: {}".format(
time, mode
)
)
self.tracklist_start_timer = Timer(time, self._tracklist_start) self.tracklist_start_timer = Timer(time, self._tracklist_start)
self.tracklist_start_timer.start() self.tracklist_start_timer.start()
elif self.tracklist_start_timer: elif self.tracklist_start_timer:
self.logger.log.error("Failed to potentially tracklist, timer already busy.") self.logger.log.error(
"Failed to potentially tracklist, timer already busy."
)
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading. # This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
def _potentially_end_tracklist(self): def _potentially_end_tracklist(self):
@ -663,24 +694,34 @@ class Player:
self.logger.log.info("No tracklist to end.") self.logger.log.info("No tracklist to end.")
return return
self.logger.log.info("Setting timer for ending tracklist_id '{}'".format(tracklist_id)) self.logger.log.info(
"Setting timer for ending tracklist_id '{}'".format(tracklist_id)
)
if tracklist_id: if tracklist_id:
self.logger.log.info("Attempting to end tracklist_id '{}'".format(tracklist_id)) self.logger.log.info(
"Attempting to end tracklist_id '{}'".format(tracklist_id)
)
if self.tracklist_end_timer: if self.tracklist_end_timer:
self.logger.log.error("Failed to potentially end tracklist, timer already busy.") self.logger.log.error(
"Failed to potentially end tracklist, timer already busy."
)
return return
self.state.update("tracklist_id", None) self.state.update("tracklist_id", None)
# This threads it, so it won't hang track loading if it fails. # This threads it, so it won't hang track loading if it fails.
self.tracklist_end_timer = Timer(1, self._tracklist_end, [tracklist_id]) self.tracklist_end_timer = Timer(1, self._tracklist_end, [tracklist_id])
self.tracklist_end_timer.start() self.tracklist_end_timer.start()
else: else:
self.logger.log.warning("Failed to potentially end tracklist, no tracklist started.") self.logger.log.warning(
"Failed to potentially end tracklist, no tracklist started."
)
def _tracklist_start(self): def _tracklist_start(self):
state = self.state.get() state = self.state.get()
loaded_item = state["loaded_item"] loaded_item = state["loaded_item"]
if not loaded_item: if not loaded_item:
self.logger.log.error("Tried to call _tracklist_start() with no loaded item!") self.logger.log.error(
"Tried to call _tracklist_start() with no loaded item!"
)
elif not self.isPlaying: elif not self.isPlaying:
self.logger.log.info("Not tracklisting since not playing.") self.logger.log.info("Not tracklisting since not playing.")
@ -688,20 +729,27 @@ class Player:
else: else:
tracklist_id = state["tracklist_id"] tracklist_id = state["tracklist_id"]
if (not tracklist_id): if not tracklist_id:
if (state["tracklist_mode"] == "fader-live" and not state["live"]): if state["tracklist_mode"] == "fader-live" and not state["live"]:
self.logger.log.info("Not tracklisting since fader is not live.") self.logger.log.info("Not tracklisting since fader is not live.")
else: else:
self.logger.log.info("Tracklisting item: '{}'".format(loaded_item.name)) self.logger.log.info(
"Tracklisting item: '{}'".format(loaded_item.name)
)
tracklist_id = self.api.post_tracklist_start(loaded_item) tracklist_id = self.api.post_tracklist_start(loaded_item)
if not tracklist_id: if not tracklist_id:
self.logger.log.warning("Failed to tracklist '{}'".format(loaded_item.name)) self.logger.log.warning(
"Failed to tracklist '{}'".format(loaded_item.name)
)
else: else:
self.logger.log.info("Tracklist id: '{}'".format(tracklist_id)) self.logger.log.info("Tracklist id: '{}'".format(tracklist_id))
self.state.update("tracklist_id", tracklist_id) self.state.update("tracklist_id", tracklist_id)
else: else:
self.logger.log.info("Not tracklisting item '{}', already got tracklistid: '{}'".format( self.logger.log.info(
loaded_item.name, tracklist_id)) "Not tracklisting item '{}', already got tracklistid: '{}'".format(
loaded_item.name, tracklist_id
)
)
# No matter what we end up doing, we need to kill this timer so future ones can run. # No matter what we end up doing, we need to kill this timer so future ones can run.
self.tracklist_start_timer = None self.tracklist_start_timer = None
@ -709,10 +757,14 @@ class Player:
def _tracklist_end(self, tracklist_id): def _tracklist_end(self, tracklist_id):
if tracklist_id: if tracklist_id:
self.logger.log.info("Attempting to end tracklist_id '{}'".format(tracklist_id)) self.logger.log.info(
"Attempting to end tracklist_id '{}'".format(tracklist_id)
)
self.api.post_tracklist_end(tracklist_id) self.api.post_tracklist_end(tracklist_id)
else: else:
self.logger.log.error("Tracklist_id to _tracklist_end() missing. Failed to end tracklist.") self.logger.log.error(
"Tracklist_id to _tracklist_end() missing. Failed to end tracklist."
)
self.tracklist_end_timer = None self.tracklist_end_timer = None
@ -727,7 +779,11 @@ class Player:
return return
# Track has ended # Track has ended
self.logger.log.info("Playback ended of {}, weight {}:".format(loaded_item.name, loaded_item.weight)) self.logger.log.info(
"Playback ended of {}, weight {}:".format(
loaded_item.name, loaded_item.weight
)
)
# Repeat 1 # Repeat 1
# TODO ENUM # TODO ENUM
@ -742,13 +798,19 @@ class Player:
# If it's been removed, weight will be -1. # If it's been removed, weight will be -1.
# Just stop in this case. # Just stop in this case.
if loaded_item.weight < 0: if loaded_item.weight < 0:
self.logger.log.debug("Loaded item is no longer in channel (weight {}), not auto advancing.".format(loaded_item.weight)) self.logger.log.debug(
"Loaded item is no longer in channel (weight {}), not auto advancing.".format(
loaded_item.weight
)
)
else: else:
self.logger.log.debug("Found current loaded item in this channel show plan. Auto Advancing.") self.logger.log.debug(
"Found current loaded item in this channel show plan. Auto Advancing."
)
# If there's another item after this one, load that. # If there's another item after this one, load that.
if len(state["show_plan"]) > loaded_item.weight+1: if len(state["show_plan"]) > loaded_item.weight + 1:
self.load(loaded_item.weight+1) self.load(loaded_item.weight + 1)
return return
# Repeat All (Jump to top again) # Repeat All (Jump to top again)
@ -795,8 +857,7 @@ class Player:
self.state.update( self.state.update(
"remaining", "remaining",
max(0, (self.state.get()["length"] - max(0, (self.state.get()["length"] - self.state.get()["pos_true"])),
self.state.get()["pos_true"])),
) )
def _ping_times(self): def _ping_times(self):
@ -832,17 +893,18 @@ class Player:
response += "FAIL" response += "FAIL"
if self.out_q: if self.out_q:
if ("STATUS:" not in response): if "STATUS:" not in response:
# Don't fill logs with status pushes, it's a mess. # Don't fill logs with status pushes, it's a mess.
self.logger.log.debug(("Sending: {}".format(response))) self.logger.log.debug(("Sending: {}".format(response)))
self.out_q.put(response) self.out_q.put(response)
else: else:
self.logger.log.exception("Message return Queue is missing!!!! Can't send message.") self.logger.log.exception(
"Message return Queue is missing!!!! Can't send message."
)
def _send_status(self): def _send_status(self):
# TODO This is hacky # TODO This is hacky
self._retMsg(str(self.status), okay_str=True, self._retMsg(str(self.status), okay_str=True, custom_prefix="ALL:STATUS:")
custom_prefix="ALL:STATUS:")
def _fix_and_update_weights(self, plan): def _fix_and_update_weights(self, plan):
def _sort_weight(e: PlanItem): def _sort_weight(e: PlanItem):
@ -854,7 +916,6 @@ class Player:
self.logger.log.debug("Weights before fixing:\n{}".format(before)) self.logger.log.debug("Weights before fixing:\n{}".format(before))
plan.sort(key=_sort_weight) # Sort into weighted order. plan.sort(key=_sort_weight) # Sort into weighted order.
sorted = [] sorted = []
@ -874,7 +935,11 @@ class Player:
self.state.update("show_plan", plan) self.state.update("show_plan", plan)
def __init__( def __init__(
self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue, server_state: StateManager self,
channel: int,
in_q: multiprocessing.Queue,
out_q: multiprocessing.Queue,
server_state: StateManager,
): ):
process_title = "Player: Channel " + str(channel) process_title = "Player: Channel " + str(channel)
@ -899,7 +964,9 @@ class Player:
self.state.update("channel", channel) self.state.update("channel", channel)
self.state.update("tracklist_mode", server_state.get()["tracklist_mode"]) self.state.update("tracklist_mode", server_state.get()["tracklist_mode"])
self.state.update("live", True) # Channel is live until controller says it isn't. self.state.update(
"live", True
) # Channel is live until controller says it isn't.
# Just in case there's any weights somehow messed up, let's fix them. # Just in case there's any weights somehow messed up, let's fix them.
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"]) plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
@ -908,8 +975,7 @@ class Player:
loaded_state = copy.copy(self.state.state) loaded_state = copy.copy(self.state.state)
if loaded_state["output"]: if loaded_state["output"]:
self.logger.log.info("Setting output to: " + self.logger.log.info("Setting output to: " + str(loaded_state["output"]))
str(loaded_state["output"]))
self.output(loaded_state["output"]) self.output(loaded_state["output"])
else: else:
self.logger.log.info("Using default output device.") self.logger.log.info("Using default output device.")
@ -918,7 +984,7 @@ class Player:
loaded_item = loaded_state["loaded_item"] loaded_item = loaded_state["loaded_item"]
if loaded_item: if loaded_item:
# No need to load on init, the output switch does this, as it would for regular output switching. # No need to load on init, the output switch does this, as it would for regular output switching.
#self.load(loaded_item.weight) # self.load(loaded_item.weight)
# Load may jump to the cue point, as it would do on a regular load. # Load may jump to the cue point, as it would do on a regular load.
# If we were at a different state before, we have to override it now. # If we were at a different state before, we have to override it now.
@ -946,8 +1012,7 @@ class Player:
self.last_msg_source = "" self.last_msg_source = ""
self.last_msg = "" self.last_msg = ""
self.logger.log.warn( self.logger.log.warn(
"Message from unknown sender source: {}".format( "Message from unknown sender source: {}".format(source)
source)
) )
continue continue
@ -981,9 +1046,13 @@ class Player:
# Unpause, so we don't jump to 0, we play from the current pos. # Unpause, so we don't jump to 0, we play from the current pos.
"PLAY": lambda: self._retMsg(self.unpause()), "PLAY": lambda: self._retMsg(self.unpause()),
"PAUSE": lambda: self._retMsg(self.pause()), "PAUSE": lambda: self._retMsg(self.pause()),
"PLAYPAUSE": lambda: self._retMsg(self.unpause() if not self.isPlaying else self.pause()), # For the hardware controller. "PLAYPAUSE": lambda: self._retMsg(
self.unpause() if not self.isPlaying else self.pause()
), # For the hardware controller.
"UNPAUSE": lambda: self._retMsg(self.unpause()), "UNPAUSE": lambda: self._retMsg(self.unpause()),
"STOP": lambda: self._retMsg(self.stop(user_initiated=True)), "STOP": lambda: self._retMsg(
self.stop(user_initiated=True)
),
"SEEK": lambda: self._retMsg( "SEEK": lambda: self._retMsg(
self.seek(float(self.last_msg.split(":")[1])) self.seek(float(self.last_msg.split(":")[1]))
), ),
@ -1011,19 +1080,33 @@ class Player:
"UNLOAD": lambda: self._retMsg(self.unload()), "UNLOAD": lambda: self._retMsg(self.unload()),
"ADD": lambda: self._retMsg( "ADD": lambda: self._retMsg(
self.add_to_plan( self.add_to_plan(
json.loads( json.loads(":".join(self.last_msg.split(":")[1:]))
":".join(self.last_msg.split(":")[1:]))
) )
), ),
"REMOVE": lambda: self._retMsg( "REMOVE": lambda: self._retMsg(
self.remove_from_plan( self.remove_from_plan(int(self.last_msg.split(":")[1]))
int(self.last_msg.split(":")[1]))
), ),
"CLEAR": lambda: self._retMsg(self.clear_channel_plan()), "CLEAR": lambda: self._retMsg(self.clear_channel_plan()),
"SETMARKER": lambda: self._retMsg(self.set_marker(self.last_msg.split(":")[1], self.last_msg.split(":", 2)[2])), "SETMARKER": lambda: self._retMsg(
"RESETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = False)), self.set_marker(
"SETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = True)), self.last_msg.split(":")[1],
"SETLIVE": lambda: self._retMsg(self.set_live(self.last_msg.split(":")[1] == "True")), self.last_msg.split(":", 2)[2],
)
),
"RESETPLAYED": lambda: self._retMsg(
self.set_played(
weight=int(self.last_msg.split(":")[1]),
played=False,
)
),
"SETPLAYED": lambda: self._retMsg(
self.set_played(
weight=int(self.last_msg.split(":")[1]), played=True
)
),
"SETLIVE": lambda: self._retMsg(
self.set_live(self.last_msg.split(":")[1] == "True")
),
} }
message_type: str = self.last_msg.split(":")[0] message_type: str = self.last_msg.split(":")[0]
@ -1051,8 +1134,7 @@ class Player:
except SystemExit: except SystemExit:
self.logger.log.info("Received SystemExit") self.logger.log.info("Received SystemExit")
except Exception as e: except Exception as e:
self.logger.log.exception( self.logger.log.exception("Received unexpected Exception: {}".format(e))
"Received unexpected Exception: {}".format(e))
self.logger.log.info("Quiting player " + str(channel)) self.logger.log.info("Quiting player " + str(channel))
self.quit() self.quit()

View file

@ -10,7 +10,9 @@ from helpers.the_terminator import Terminator
class PlayerHandler: class PlayerHandler:
logger: LoggingManager logger: LoggingManager
def __init__(self, channel_from_q, websocket_to_q, ui_to_q, controller_to_q, file_to_q): def __init__(
self, channel_from_q, websocket_to_q, ui_to_q, controller_to_q, file_to_q
):
self.logger = LoggingManager("PlayerHandler") self.logger = LoggingManager("PlayerHandler")
process_title = "Player Handler" process_title = "Player Handler"
@ -31,7 +33,6 @@ class PlayerHandler:
if command == "GET_PLAN" or command == "STATUS": if command == "GET_PLAN" or command == "STATUS":
file_to_q[channel].put(message) file_to_q[channel].put(message)
# TODO ENUM # TODO ENUM
if source in ["ALL", "WEBSOCKET"]: if source in ["ALL", "WEBSOCKET"]:
websocket_to_q[channel].put(message) websocket_to_q[channel].put(message)
@ -46,7 +47,6 @@ class PlayerHandler:
sleep(0.02) sleep(0.02)
except Exception as e: except Exception as e:
self.logger.log.exception( self.logger.log.exception("Received unexpected exception: {}".format(e))
"Received unexpected exception: {}".format(e))
del self.logger del self.logger
_exit(0) _exit(0)

View file

@ -110,28 +110,53 @@ class BAPSicleServer:
terminator = Terminator() terminator = Terminator()
log_function = self.logger.log.info log_function = self.logger.log.info
while not terminator.terminate and self.state.get()["running_state"] == "running": while (
not terminator.terminate and self.state.get()["running_state"] == "running"
):
for channel in range(self.state.get()["num_channels"]): for channel in range(self.state.get()["num_channels"]):
# Use pid_exists to confirm process is actually still running. Python may not report is_alive() correctly (especially over system sleeps etc.) # Use pid_exists to confirm process is actually still running. Python may not report is_alive() correctly (especially over system sleeps etc.)
# https://medium.com/pipedrive-engineering/encountering-some-python-trickery-683bd5f66750 # https://medium.com/pipedrive-engineering/encountering-some-python-trickery-683bd5f66750
if not self.player[channel] or not self.player[channel].is_alive() or not psutil.pid_exists(self.player[channel].pid): if (
not self.player[channel]
or not self.player[channel].is_alive()
or not psutil.pid_exists(self.player[channel].pid)
):
log_function("Player {} not running, (re)starting.".format(channel)) log_function("Player {} not running, (re)starting.".format(channel))
self.player[channel] = multiprocessing.Process( self.player[channel] = multiprocessing.Process(
target=player.Player, target=player.Player,
args=(channel, self.player_to_q[channel], self.player_from_q[channel], self.state) args=(
channel,
self.player_to_q[channel],
self.player_from_q[channel],
self.state,
),
) )
self.player[channel].start() self.player[channel].start()
if not self.player_handler or not self.player_handler.is_alive() or not psutil.pid_exists(self.player_handler.pid): if (
not self.player_handler
or not self.player_handler.is_alive()
or not psutil.pid_exists(self.player_handler.pid)
):
log_function("Player Handler not running, (re)starting.") log_function("Player Handler not running, (re)starting.")
self.player_handler = multiprocessing.Process( self.player_handler = multiprocessing.Process(
target=PlayerHandler, target=PlayerHandler,
args=(self.player_from_q, self.websocket_to_q, self.ui_to_q, self.controller_to_q, self.file_to_q), args=(
self.player_from_q,
self.websocket_to_q,
self.ui_to_q,
self.controller_to_q,
self.file_to_q,
),
) )
self.player_handler.start() self.player_handler.start()
if not self.file_manager or not self.file_manager.is_alive() or not psutil.pid_exists(self.file_manager.pid): if (
not self.file_manager
or not self.file_manager.is_alive()
or not psutil.pid_exists(self.file_manager.pid)
):
log_function("File Manager not running, (re)starting.") log_function("File Manager not running, (re)starting.")
self.file_manager = multiprocessing.Process( self.file_manager = multiprocessing.Process(
target=FileManager, target=FileManager,
@ -139,24 +164,38 @@ class BAPSicleServer:
) )
self.file_manager.start() self.file_manager.start()
if not self.websockets_server or not self.websockets_server.is_alive() or not psutil.pid_exists(self.websockets_server.pid): if (
not self.websockets_server
or not self.websockets_server.is_alive()
or not psutil.pid_exists(self.websockets_server.pid)
):
log_function("Websocket Server not running, (re)starting.") log_function("Websocket Server not running, (re)starting.")
self.websockets_server = multiprocessing.Process( self.websockets_server = multiprocessing.Process(
target=WebsocketServer, args=(self.player_to_q, self.websocket_to_q, self.state) target=WebsocketServer,
args=(self.player_to_q, self.websocket_to_q, self.state),
) )
self.websockets_server.start() self.websockets_server.start()
if not self.webserver or not self.webserver.is_alive() or not psutil.pid_exists(self.webserver.pid): if (
not self.webserver
or not self.webserver.is_alive()
or not psutil.pid_exists(self.webserver.pid)
):
log_function("Webserver not running, (re)starting.") log_function("Webserver not running, (re)starting.")
self.webserver = multiprocessing.Process( self.webserver = multiprocessing.Process(
target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state) target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state)
) )
self.webserver.start() self.webserver.start()
if not self.controller_handler or not self.controller_handler.is_alive() or not psutil.pid_exists(self.controller_handler.pid): if (
not self.controller_handler
or not self.controller_handler.is_alive()
or not psutil.pid_exists(self.controller_handler.pid)
):
log_function("Controller Handler not running, (re)starting.") log_function("Controller Handler not running, (re)starting.")
self.controller_handler = multiprocessing.Process( self.controller_handler = multiprocessing.Process(
target=MattchBox, args=(self.player_to_q, self.controller_to_q, self.state) target=MattchBox,
args=(self.player_to_q, self.controller_to_q, self.state),
) )
self.controller_handler.start() self.controller_handler.start()
@ -179,7 +218,9 @@ class BAPSicleServer:
ProxyManager.register("StateManager", StateManager) ProxyManager.register("StateManager", StateManager)
manager = ProxyManager() manager = ProxyManager()
manager.start() manager.start()
self.state: StateManager = manager.StateManager("BAPSicleServer", self.logger, self.default_state) self.state: StateManager = manager.StateManager(
"BAPSicleServer", self.logger, self.default_state
)
self.state.update("running_state", "running") self.state.update("running_state", "running")
@ -203,8 +244,16 @@ class BAPSicleServer:
self.controller_to_q.append(multiprocessing.Queue()) self.controller_to_q.append(multiprocessing.Queue())
self.file_to_q.append(multiprocessing.Queue()) self.file_to_q.append(multiprocessing.Queue())
print("Welcome to BAPSicle Server version: {}, build: {}.".format(package.VERSION, package.BUILD)) print(
print("The Server UI is available at http://{}:{}".format(self.state.get()["host"], self.state.get()["port"])) "Welcome to BAPSicle Server version: {}, build: {}.".format(
package.VERSION, package.BUILD
)
)
print(
"The Server UI is available at http://{}:{}".format(
self.state.get()["host"], self.state.get()["port"]
)
)
# TODO Move this to player or installer. # TODO Move this to player or installer.
if False: if False:

View file

@ -7,4 +7,5 @@ setup(
description=package.DESCRIPTION, description=package.DESCRIPTION,
author=package.AUTHOR, author=package.AUTHOR,
license=package.LICENSE, license=package.LICENSE,
packages=find_packages()) packages=find_packages(),
)

View file

@ -68,7 +68,9 @@ class TestPlayer(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.logger = LoggingManager("Test_Player") cls.logger = LoggingManager("Test_Player")
cls.server_state = StateManager("BAPSicleServer", cls.logger, default_state={"tracklist_mode": "off"}) # Mostly dummy here. cls.server_state = StateManager(
"BAPSicleServer", cls.logger, default_state={"tracklist_mode": "off"}
) # Mostly dummy here.
# clean up logic for the test suite declared in the test module # clean up logic for the test suite declared in the test module
# code that is executed after all tests in one test run # code that is executed after all tests in one test run
@ -82,7 +84,8 @@ class TestPlayer(unittest.TestCase):
self.player_from_q = multiprocessing.Queue() self.player_from_q = multiprocessing.Queue()
self.player_to_q = multiprocessing.Queue() self.player_to_q = multiprocessing.Queue()
self.player = multiprocessing.Process( self.player = multiprocessing.Process(
target=Player, args=(-1, self.player_to_q, self.player_from_q, self.server_state) target=Player,
args=(-1, self.player_to_q, self.player_from_q, self.server_state),
) )
self.player.start() self.player.start()
self._send_msg_wait_OKAY("CLEAR") # Empty any previous track items. self._send_msg_wait_OKAY("CLEAR") # Empty any previous track items.
@ -125,7 +128,7 @@ class TestPlayer(unittest.TestCase):
source = response[: response.index(":")] source = response[: response.index(":")]
if source in sources_filter: if source in sources_filter:
return response[ return response[
len(source + ":" + msg) + 1: len(source + ":" + msg) + 1 :
] # +1 to remove trailing : on source. ] # +1 to remove trailing : on source.
except Empty: except Empty:
pass pass
@ -339,9 +342,13 @@ class TestPlayer(unittest.TestCase):
# Now test that all the markers we setup are present. # Now test that all the markers we setup are present.
item = json_obj["show_plan"][0] item = json_obj["show_plan"][0]
self.assertEqual(item["weight"], 0) self.assertEqual(item["weight"], 0)
self.assertEqual(item["intro"], 2.0) # Backwards compat with basic Webstudio intro/cue/outro self.assertEqual(
item["intro"], 2.0
) # Backwards compat with basic Webstudio intro/cue/outro
self.assertEqual(item["cue"], 3.14) self.assertEqual(item["cue"], 3.14)
self.assertEqual([json.dumps(item) for item in item["markers"]], markers[0:2]) # Check the full marker configs match self.assertEqual(
[json.dumps(item) for item in item["markers"]], markers[0:2]
) # Check the full marker configs match
item = json_obj["show_plan"][1] item = json_obj["show_plan"][1]
self.assertEqual(item["weight"], 1) self.assertEqual(item["weight"], 1)
@ -355,7 +362,9 @@ class TestPlayer(unittest.TestCase):
self.assertEqual(item["intro"], 0.0) self.assertEqual(item["intro"], 0.0)
self.assertEqual(item["outro"], 0.0) self.assertEqual(item["outro"], 0.0)
self.assertEqual(item["cue"], 0.0) self.assertEqual(item["cue"], 0.0)
self.assertEqual([json.dumps(item) for item in item["markers"]], markers[3:]) self.assertEqual(
[json.dumps(item) for item in item["markers"]], markers[3:]
)
# TODO: Now test editing/deleting them # TODO: Now test editing/deleting them

View file

@ -16,7 +16,11 @@ from time import sleep
import json import json
import os import os
from helpers.os_environment import isBundelled, resolve_external_file_path, resolve_local_file_path from helpers.os_environment import (
isBundelled,
resolve_external_file_path,
resolve_local_file_path,
)
from helpers.logging_manager import LoggingManager from helpers.logging_manager import LoggingManager
from helpers.device_manager import DeviceManager from helpers.device_manager import DeviceManager
from helpers.state_manager import StateManager from helpers.state_manager import StateManager
@ -24,7 +28,10 @@ from helpers.the_terminator import Terminator
from helpers.normalisation import get_normalised_filename_if_available from helpers.normalisation import get_normalised_filename_if_available
from helpers.myradio_api import MyRadioAPI from helpers.myradio_api import MyRadioAPI
env = Environment(loader=FileSystemLoader('%s/ui-templates/' % os.path.dirname(__file__)), autoescape=select_autoescape()) env = Environment(
loader=FileSystemLoader("%s/ui-templates/" % os.path.dirname(__file__)),
autoescape=select_autoescape(),
)
LOG_FILEPATH = resolve_external_file_path("logs") LOG_FILEPATH = resolve_external_file_path("logs")
LOG_FILENAME = LOG_FILEPATH + "/WebServer.log" LOG_FILENAME = LOG_FILEPATH + "/WebServer.log"
@ -52,17 +59,17 @@ LOGGING_CONFIG = dict(
"file": { "file": {
"class": "logging.FileHandler", "class": "logging.FileHandler",
"formatter": "generic", "formatter": "generic",
"filename": LOG_FILENAME "filename": LOG_FILENAME,
}, },
"error_file": { "error_file": {
"class": "logging.FileHandler", "class": "logging.FileHandler",
"formatter": "generic", "formatter": "generic",
"filename": LOG_FILENAME "filename": LOG_FILENAME,
}, },
"access_file": { "access_file": {
"class": "logging.FileHandler", "class": "logging.FileHandler",
"formatter": "access", "formatter": "access",
"filename": LOG_FILENAME "filename": LOG_FILENAME,
}, },
}, },
formatters={ formatters={
@ -113,7 +120,7 @@ def ui_index(request):
"server_build": config["server_build"], "server_build": config["server_build"],
"server_name": config["server_name"], "server_name": config["server_name"],
"server_beta": config["server_beta"], "server_beta": config["server_beta"],
"server_branch": config["server_branch"] "server_branch": config["server_branch"],
} }
return render_template("index.html", data=data) return render_template("index.html", data=data)
@ -124,8 +131,7 @@ def ui_status(request):
for i in range(server_state.get()["num_channels"]): for i in range(server_state.get()["num_channels"]):
channel_states.append(status(i)) channel_states.append(status(i))
data = {"channels": channel_states, data = {"channels": channel_states, "ui_page": "status", "ui_title": "Status"}
"ui_page": "status", "ui_title": "Status"}
return render_template("status.html", data=data) return render_template("status.html", data=data)
@ -153,7 +159,7 @@ def ui_config_server(request):
"ui_title": "Server Config", "ui_title": "Server Config",
"state": server_state.get(), "state": server_state.get(),
"ser_ports": DeviceManager.getSerialPorts(), "ser_ports": DeviceManager.getSerialPorts(),
"tracklist_modes": ["off", "on", "delayed", "fader-live"] "tracklist_modes": ["off", "on", "delayed", "fader-live"],
} }
return render_template("config_server.html", data=data) return render_template("config_server.html", data=data)
@ -177,7 +183,9 @@ def ui_config_server_update(request):
server_state.update("myradio_base_url", request.form.get("myradio_base_url")) server_state.update("myradio_base_url", request.form.get("myradio_base_url"))
server_state.update("myradio_api_url", request.form.get("myradio_api_url")) server_state.update("myradio_api_url", request.form.get("myradio_api_url"))
server_state.update("myradio_api_tracklist_source", request.form.get("myradio_api_tracklist_source")) server_state.update(
"myradio_api_tracklist_source", request.form.get("myradio_api_tracklist_source")
)
server_state.update("tracklist_mode", request.form.get("tracklist_mode")) server_state.update("tracklist_mode", request.form.get("tracklist_mode"))
return redirect("/restart") return redirect("/restart")
@ -192,11 +200,7 @@ def ui_logs_list(request):
log_files.append(file.rstrip(".log")) log_files.append(file.rstrip(".log"))
log_files.sort() log_files.sort()
data = { data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files}
"ui_page": "logs",
"ui_title": "Logs",
"logs": log_files
}
return render_template("loglist.html", data=data) return render_template("loglist.html", data=data)
@ -210,10 +214,12 @@ def ui_logs_render(request, path):
log_file = open(resolve_external_file_path("/logs/{}.log").format(path)) log_file = open(resolve_external_file_path("/logs/{}.log").format(path))
data = { data = {
"logs": log_file.read().splitlines()[-300*page:(-300*(page-1) if page > 1 else None)][::-1], "logs": log_file.read().splitlines()[
-300 * page : (-300 * (page - 1) if page > 1 else None)
][::-1],
"ui_page": "logs", "ui_page": "logs",
"ui_title": "Logs - {}".format(path), "ui_title": "Logs - {}".format(path),
"page": page "page": page,
} }
log_file.close() log_file.close()
return render_template("log.html", data=data) return render_template("log.html", data=data)
@ -296,6 +302,7 @@ def player_all_stop(request):
# Show Plan Functions # Show Plan Functions
@app.route("/plan/load/<timeslotid:int>") @app.route("/plan/load/<timeslotid:int>")
def plan_load(request, timeslotid: int): def plan_load(request, timeslotid: int):
@ -314,6 +321,7 @@ def plan_clear(request):
# API Proxy Endpoints # API Proxy Endpoints
@app.route("/plan/list") @app.route("/plan/list")
async def api_list_showplans(request): async def api_list_showplans(request):
@ -323,7 +331,11 @@ async def api_list_showplans(request):
@app.route("/library/search/track") @app.route("/library/search/track")
async def api_search_library(request): async def api_search_library(request):
return resp_json(await api.get_track_search(request.args.get("title"), request.args.get("artist"))) return resp_json(
await api.get_track_search(
request.args.get("title"), request.args.get("artist")
)
)
@app.route("/library/playlists/<type:string>") @app.route("/library/playlists/<type:string>")
@ -368,7 +380,7 @@ def json_status(request):
async def audio_file(request, type: str, id: int): async def audio_file(request, type: str, id: int):
if type not in ["managed", "track"]: if type not in ["managed", "track"]:
abort(404) abort(404)
filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format(type,id)) filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format(type, id))
# Swap with a normalised version if it's ready, else returns original. # Swap with a normalised version if it's ready, else returns original.
filename = get_normalised_filename_if_available(filename) filename = get_normalised_filename_if_available(filename)
@ -378,18 +390,25 @@ async def audio_file(request, type: str, id: int):
# Static Files # Static Files
app.static("/favicon.ico", resolve_local_file_path("ui-static/favicon.ico"), name="ui-favicon") app.static(
"/favicon.ico", resolve_local_file_path("ui-static/favicon.ico"), name="ui-favicon"
)
app.static("/static", resolve_local_file_path("ui-static"), name="ui-static") app.static("/static", resolve_local_file_path("ui-static"), name="ui-static")
dist_directory = resolve_local_file_path("presenter-build") dist_directory = resolve_local_file_path("presenter-build")
app.static('/presenter', dist_directory) app.static("/presenter", dist_directory)
app.static("/presenter/", resolve_local_file_path("presenter-build/index.html"), app.static(
strict_slashes=True, name="presenter-index") "/presenter/",
resolve_local_file_path("presenter-build/index.html"),
strict_slashes=True,
name="presenter-index",
)
# Helper Functions # Helper Functions
def status(channel: int): def status(channel: int):
while not player_from_q[channel].empty(): while not player_from_q[channel].empty():
player_from_q[channel].get() # Just waste any previous status responses. player_from_q[channel].get() # Just waste any previous status responses.
@ -402,7 +421,7 @@ def status(channel: int):
if response.startswith("UI:STATUS:"): if response.startswith("UI:STATUS:"):
response = response.split(":", 2)[2] response = response.split(":", 2)[2]
# TODO: Handle OKAY / FAIL # TODO: Handle OKAY / FAIL
response = response[response.index(":") + 1:] response = response[response.index(":") + 1 :]
try: try:
response = json.loads(response) response = json.loads(response)
except Exception as e: except Exception as e:
@ -416,6 +435,7 @@ def status(channel: int):
sleep(0.02) sleep(0.02)
# WebServer Start / Stop Functions # WebServer Start / Stop Functions
@ -428,7 +448,7 @@ def quit(request):
"ui_title": "Quitting BAPSicle", "ui_title": "Quitting BAPSicle",
"title": "See you later!", "title": "See you later!",
"ui_menu": False, "ui_menu": False,
"message": "BAPSicle is going back into winter hibernation, see you again soon!" "message": "BAPSicle is going back into winter hibernation, see you again soon!",
} }
return render_template("message.html", data) return render_template("message.html", data)
@ -444,7 +464,7 @@ def restart(request):
"ui_menu": False, "ui_menu": False,
"message": "Just putting BAPSicle back in the freezer for a moment!", "message": "Just putting BAPSicle back in the freezer for a moment!",
"redirect_to": "/", "redirect_to": "/",
"redirect_wait_ms": 10000 "redirect_wait_ms": 10000,
} }
return render_template("message.html", data) return render_template("message.html", data)
@ -467,13 +487,15 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana
terminate = Terminator() terminate = Terminator()
while not terminate.terminate: while not terminate.terminate:
try: try:
sync(app.run( sync(
app.run(
host=server_state.get()["host"], host=server_state.get()["host"],
port=server_state.get()["port"], port=server_state.get()["port"],
debug=(not isBundelled()), debug=(not isBundelled()),
auto_reload=False, auto_reload=False,
access_log=(not isBundelled()) access_log=(not isBundelled()),
)) )
)
except Exception: except Exception:
break break
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View file

@ -74,9 +74,7 @@ class WebsocketServer:
self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket)) self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket))
try: try:
self.threads = await shield( self.threads = await shield(asyncio.gather(self.from_webstudio))
asyncio.gather(self.from_webstudio)
)
finally: finally:
self.from_webstudio.cancel() self.from_webstudio.cancel()
@ -92,13 +90,10 @@ class WebsocketServer:
channel = int(data["channel"]) channel = int(data["channel"])
self.sendCommand(channel, data) self.sendCommand(channel, data)
await asyncio.wait( await asyncio.wait([conn.send(message) for conn in self.baps_clients])
[conn.send(message) for conn in self.baps_clients]
)
except websockets.exceptions.ConnectionClosedError as e: except websockets.exceptions.ConnectionClosedError as e:
self.logger.log.error( self.logger.log.error("Client Disconncted {}, {}".format(websocket, e))
"Client Disconncted {}, {}".format(websocket, e))
except Exception as e: except Exception as e:
self.logger.log.exception( self.logger.log.exception(
@ -152,8 +147,7 @@ class WebsocketServer:
extra += str(data["timeslotId"]) extra += str(data["timeslotId"])
elif command == "SETMARKER": elif command == "SETMARKER":
extra += "{}:{}".format( extra += "{}:{}".format(
data["timeslotitemid"], data["timeslotitemid"], json.dumps(data["marker"])
json.dumps(data["marker"])
) )
# TODO: Move this to player handler. # TODO: Move this to player handler.
@ -174,21 +168,22 @@ class WebsocketServer:
# Now send the special case. # Now send the special case.
self.channel_to_q[new_channel].put( self.channel_to_q[new_channel].put(
"WEBSOCKET:ADD:" + json.dumps(item)) "WEBSOCKET:ADD:" + json.dumps(item)
)
# Don't bother, we should be done. # Don't bother, we should be done.
return return
except ValueError as e: except ValueError as e:
self.logger.log.exception( self.logger.log.exception(
"Error decoding extra data {} for command {} ".format( "Error decoding extra data {} for command {} ".format(e, command)
e, command
)
) )
pass pass
# Stick the message together and send! # Stick the message together and send!
message += command # Put the command in at the end, in case MOVE etc changed it. message += (
command # Put the command in at the end, in case MOVE etc changed it.
)
if extra != "": if extra != "":
message += ":" + extra message += ":" + extra
@ -202,9 +197,7 @@ class WebsocketServer:
) )
else: else:
self.logger.log.error( self.logger.log.error("Command missing from message. Data: {}".format(data))
"Command missing from message. Data: {}".format(data)
)
async def handle_to_webstudio(self): async def handle_to_webstudio(self):
@ -244,9 +237,7 @@ class WebsocketServer:
data = json.dumps( data = json.dumps(
{"command": command, "data": message, "channel": channel} {"command": command, "data": message, "channel": channel}
) )
await asyncio.wait( await asyncio.wait([conn.send(data) for conn in self.baps_clients])
[conn.send(data) for conn in self.baps_clients]
)
except queue.Empty: except queue.Empty:
continue continue
except ValueError: except ValueError: