From 2941d90f60d253783109253ef3fdde8e944f26f8 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 11 Sep 2021 16:49:08 +0100 Subject: [PATCH] Lint with black for formatting. --- baps_types/plan.py | 54 +++++-- controllers/mattchbox_usb.py | 13 +- file_manager.py | 296 +++++++++++++++++++---------------- helpers/device_manager.py | 5 +- helpers/myradio_api.py | 29 ++-- helpers/normalisation.py | 63 ++++---- helpers/state_manager.py | 44 ++++-- launch.py | 6 +- package.py | 3 +- player.py | 254 ++++++++++++++++++++---------- player_handler.py | 8 +- server.py | 77 +++++++-- setup.py | 3 +- tests/test_player.py | 21 ++- web_server.py | 88 +++++++---- websocket_server.py | 33 ++-- 16 files changed, 623 insertions(+), 374 deletions(-) diff --git a/baps_types/plan.py b/baps_types/plan.py index f770e3a..e5afbbd 100644 --- a/baps_types/plan.py +++ b/baps_types/plan.py @@ -20,8 +20,11 @@ from time import time from baps_types.marker import Marker + def _time_ms(): return round(time() * 1000) + + class PlanItem: _timeslotitemid: str = "0" _weight: int = 0 @@ -72,7 +75,7 @@ class PlanItem: self._played_at = _time_ms() 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: self._played_at = 0 @@ -118,7 +121,9 @@ class PlanItem: @property 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?) if len(markers) > 0: return markers[0].time @@ -126,7 +131,9 @@ class PlanItem: @property 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?) if len(markers) > 0: return markers[0].time @@ -134,7 +141,9 @@ class PlanItem: @property 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?) if len(markers) > 0: return markers[0].time @@ -164,7 +173,7 @@ class PlanItem: "played": self.play_count > 0, "played_at": self.played_at, "play_count": self.play_count, - "clean": self.clean + "clean": self.clean, } 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._length = new_item["length"] 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._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 # 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 = { "name": "Intro", "time": new_item["intro"], "position": "start", - "section": None + "section": None, } 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 = { "name": "Cue", "time": new_item["cue"], "position": "mid", - "section": None + "section": None, } self.set_marker(Marker(json.dumps(marker))) # 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 = { "name": "Outro", "time": new_item["outro"], "position": "end", - "section": None + "section": None, } self.set_marker(Marker(json.dumps(marker))) - # Fix any OS specific / or \'s if self.filename: if os.path.sep == "/": diff --git a/controllers/mattchbox_usb.py b/controllers/mattchbox_usb.py index 1df27ca..c7dcc11 100644 --- a/controllers/mattchbox_usb.py +++ b/controllers/mattchbox_usb.py @@ -1,4 +1,3 @@ - from helpers.the_terminator import Terminator from typing import List, Optional from multiprocessing import Queue, current_process @@ -95,12 +94,12 @@ class MattchBox(Controller): self.logger.log.info("Received from controller: " + str(line)) if line == 255: 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. - self.sendToPlayer(line-51, "SETLIVE:False") - elif line in [61,62,63]: + self.sendToPlayer(line - 51, "SETLIVE:False") + elif line in [61, 62, 63]: # 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]: self.sendToPlayer(int(line / 2), "PLAYPAUSE") elif line in [2, 4, 6]: @@ -136,5 +135,7 @@ class MattchBox(Controller): self.connect(None) 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) diff --git a/file_manager.py b/file_manager.py index 96ad701..a6cca0f 100644 --- a/file_manager.py +++ b/file_manager.py @@ -31,193 +31,223 @@ class FileManager: terminator = Terminator() self.channel_count = len(channel_from_q) 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.known_channels_preloaded = [False]*self.channel_count - self.known_channels_normalised = [False]*self.channel_count - self.last_known_item_ids = [[]]*self.channel_count + self.known_channels_preloaded = [False] * self.channel_count + self.known_channels_normalised = [False] * self.channel_count + self.last_known_item_ids = [[]] * self.channel_count try: while not terminator.terminate: # 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): - self.channel_received = [False]*self.channel_count + if ( + 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): try: message = channel_from_q[channel].get_nowait() except Exception: - continue + continue try: - #source = message.split(":")[0] - command = message.split(":",2)[1] + # source = message.split(":")[0] + command = message.split(":", 2)[1] # If we have requested a new show plan, empty the music-tmp directory for the previous show. if command == "GETPLAN": - 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. - # If the channel was already in the process of being deleted, the user has requested it again, so allow it. + 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. + # If the channel was already in the process of being deleted, the user has requested it again, so allow it. + self.channel_received[channel] = True + continue + + # Delete the previous show files! + # Note: The players load into RAM. If something is playing over the load, the source file can still be deleted. + path: str = resolve_external_file_path("/music-tmp/") + + if not os.path.isdir(path): + self.logger.log.warning( + "Music-tmp folder is missing, not handling." + ) + continue + + files = [ + f + for f in os.listdir(path) + if os.path.isfile(os.path.join(path, f)) + ] + for file in files: + if isWindows(): + filepath = path + "\\" + file + else: + filepath = path + "/" + file + self.logger.log.info( + "Removing file {} on new show load.".format( + filepath + ) + ) + try: + os.remove(filepath) + except Exception: + self.logger.log.warning( + "Failed to remove, skipping. Likely file is still in use." + ) + continue self.channel_received[channel] = True - continue - - # Delete the previous show files! - # Note: The players load into RAM. If something is playing over the load, the source file can still be deleted. - path: str = resolve_external_file_path("/music-tmp/") - - if not os.path.isdir(path): - self.logger.log.warning("Music-tmp folder is missing, not handling.") - continue - - files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] - for file in files: - if isWindows(): - filepath = path+"\\"+file - else: - filepath = path+"/"+file - self.logger.log.info("Removing file {} on new show load.".format(filepath)) - try: - os.remove(filepath) - except Exception: - self.logger.log.warning("Failed to remove, skipping. Likely file is still in use.") - continue - self.channel_received[channel] = True - self.known_channels_preloaded = [False]*self.channel_count - self.known_channels_normalised = [False]*self.channel_count + self.known_channels_preloaded = [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 command == "STATUS": - extra = message.split(":",3) - if extra[2] != "OKAY": - continue + extra = message.split(":", 3) + if extra[2] != "OKAY": + continue - status = json.loads(extra[3]) - show_plan = status["show_plan"] - item_ids = [] - for item in show_plan: - item_ids += item["timeslotitemid"] + status = json.loads(extra[3]) + show_plan = status["show_plan"] + item_ids = [] + for item in show_plan: + item_ids += item["timeslotitemid"] - # If the new status update has a different order / list of items, let's update the show plan we know about - # This will trigger the chunk below to do the rounds again and preload any new files. - if item_ids != self.last_known_item_ids[channel]: - self.last_known_item_ids[channel] = item_ids - self.last_known_show_plan[channel] = show_plan - self.known_channels_preloaded[channel] = False + # If the new status update has a different order / list of items, let's update the show plan we know about + # This will trigger the chunk below to do the rounds again and preload any new files. + if item_ids != self.last_known_item_ids[channel]: + self.last_known_item_ids[channel] = item_ids + self.last_known_show_plan[channel] = show_plan + self.known_channels_preloaded[channel] = False 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. preloaded = self.do_preload() normalised = self.do_normalise() - if (not preloaded and not normalised): - # We didn't do any hard work, let's sleep. - sleep(0.2) + if not preloaded and not normalised: + # We didn't do any hard work, let's sleep. + sleep(0.2) except Exception as e: - self.logger.log.exception( - "Received unexpected exception: {}".format(e)) + self.logger.log.exception("Received unexpected exception: {}".format(e)) del self.logger - # Attempt to preload a file onto disk. def do_preload(self): - channel = self.next_channel_preload + channel = self.next_channel_preload - # All channels have preloaded all files, do nothing. - if (self.known_channels_preloaded == [True]*self.channel_count): - return False # Didn't preload anything + # All channels have preloaded all files, do nothing. + if self.known_channels_preloaded == [True] * self.channel_count: + return False # Didn't preload anything - # Right, let's have a quick check in the status for shows without filenames, to preload them. - # Keep an eye on if we downloaded anything. - # If we didn't, we know that all items in this channel have been downloaded. - downloaded_something = False - for i in range(len(self.last_known_show_plan[channel])): + # Right, let's have a quick check in the status for shows without filenames, to preload them. + # Keep an eye on if we downloaded anything. + # If we didn't, we know that all items in this channel have been downloaded. + downloaded_something = False + for i in range(len(self.last_known_show_plan[channel])): - item_obj = PlanItem(self.last_known_show_plan[channel][i]) + item_obj = PlanItem(self.last_known_show_plan[channel][i]) - # We've not downloaded this file yet, let's do that. - if not item_obj.filename: - self.logger.log.info("Checking pre-load on channel {}, weight {}: {}".format(channel, item_obj.weight, item_obj.name)) + # We've not downloaded this file yet, let's do that. + if not item_obj.filename: + 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. - 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. + # 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) + ) + # 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 - self.last_known_show_plan[channel][i] = item_obj.__dict__ + # Save back the resulting item back in regular dict form + self.last_known_show_plan[channel][i] = item_obj.__dict__ - if did_download: - downloaded_something = True - self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename)) - break - else: - # We didn't download anything this time, file was already loaded. - # Let's try the next one. - continue + if did_download: + downloaded_something = True + self.logger.log.info( + "File successfully preloaded: {}".format(item_obj.filename) + ) + break + else: + # We didn't download anything this time, file was already loaded. + # Let's try the next one. + continue - # Tell the file manager that this channel is fully downloaded, this is so it can consider normalising once all channels have files. - self.known_channels_preloaded[channel] = not downloaded_something + # Tell the file manager that this channel is fully downloaded, this is so it can consider normalising once all channels have files. + self.known_channels_preloaded[channel] = not downloaded_something - self.next_channel_preload += 1 - if self.next_channel_preload >= self.channel_count: - self.next_channel_preload = 0 - - return downloaded_something + self.next_channel_preload += 1 + if self.next_channel_preload >= self.channel_count: + self.next_channel_preload = 0 + return downloaded_something # If we've preloaded everything, get to work normalising tracks before playback. def do_normalise(self): - # Some channels still have files to preload, do nothing. - if (self.known_channels_preloaded != [True]*self.channel_count): - return False # Didn't normalise + # Some channels still have files to preload, do nothing. + if self.known_channels_preloaded != [True] * self.channel_count: + return False # Didn't normalise - # Quit early if all channels are normalised already. - if (self.known_channels_normalised == [True]*self.channel_count): - return False + # Quit early if all channels are normalised already. + if self.known_channels_normalised == [True] * self.channel_count: + return False - channel = self.next_channel_preload + channel = self.next_channel_preload - normalised_something = False - # Look through all the show plan files - for i in range(len(self.last_known_show_plan[channel])): - - item_obj = PlanItem(self.last_known_show_plan[channel][i]) - - filename = item_obj.filename - if not filename: - self.logger.log.exception("Somehow got empty filename when all channels are preloaded.") - continue # Try next song. - elif (not os.path.isfile(filename)): - self.logger.log.exception("Filename for normalisation does not exist. This is bad.") - continue - elif "normalised" in filename: - continue - # Sweet, we now need to try generating a normalised version. - try: - self.logger.log.info("Normalising on channel {}: {}".format(channel,filename)) - # This will return immediately if we already have a normalised file. - item_obj.filename = generate_normalised_file(filename) - # TODO Hacky - self.last_known_show_plan[channel][i] = item_obj.__dict__ - normalised_something = True - break # Now go let another channel have a go. - except Exception as e: - self.logger.log.exception("Failed to generate normalised file.", str(e)) - continue - - self.known_channels_normalised[channel] = not normalised_something - - self.next_channel_preload += 1 - if self.next_channel_preload >= self.channel_count: - self.next_channel_preload = 0 - - return normalised_something + normalised_something = False + # Look through all the show plan files + for i in range(len(self.last_known_show_plan[channel])): + item_obj = PlanItem(self.last_known_show_plan[channel][i]) + filename = item_obj.filename + if not filename: + self.logger.log.exception( + "Somehow got empty filename when all channels are preloaded." + ) + continue # Try next song. + elif not os.path.isfile(filename): + self.logger.log.exception( + "Filename for normalisation does not exist. This is bad." + ) + continue + elif "normalised" in filename: + continue + # Sweet, we now need to try generating a normalised version. + try: + self.logger.log.info( + "Normalising on channel {}: {}".format(channel, filename) + ) + # This will return immediately if we already have a normalised file. + item_obj.filename = generate_normalised_file(filename) + # TODO Hacky + self.last_known_show_plan[channel][i] = item_obj.__dict__ + normalised_something = True + break # Now go let another channel have a go. + except Exception as e: + self.logger.log.exception("Failed to generate normalised file.", str(e)) + continue + self.known_channels_normalised[channel] = not normalised_something + self.next_channel_preload += 1 + if self.next_channel_preload >= self.channel_count: + self.next_channel_preload = 0 + return normalised_something diff --git a/helpers/device_manager.py b/helpers/device_manager.py index 889d190..ae5c930 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple import sounddevice as sd from helpers.os_environment import isLinux, isMacOS, isWindows import glob + if isWindows(): from serial.tools.list_ports_windows import comports @@ -39,7 +40,9 @@ class DeviceManager: else: 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 = sorted(outputs, key=lambda k: k["name"]) diff --git a/helpers/myradio_api.py b/helpers/myradio_api.py index 070c66f..77e8f65 100644 --- a/helpers/myradio_api.py +++ b/helpers/myradio_api.py @@ -56,7 +56,8 @@ class MyRadioAPI: async with func as response: if response.status != status_code: 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())) return await response.read() @@ -81,7 +82,9 @@ class MyRadioAPI: self._logException(str(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": url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url) elif api_version == "non": @@ -198,7 +201,6 @@ class MyRadioAPI: self.logger.log.error("Show plan in unknown format.") return None - # Audio Library 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 os.path.isfile(filename + dl_suffix): 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: # 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 os.path.isfile(filename): # Now the file is downloaded successfully return (filename, False) if did_download else filename - time_waiting_s +=1 - self._log("Still waiting",DEBUG) + time_waiting_s += 1 + self._log("Still waiting", DEBUG) time.sleep(1) # File doesn't exist, download it. @@ -300,7 +305,7 @@ class MyRadioAPI: async def get_playlist_aux_items(self, library_id: str): # Sometimes they have "aux-", we only need the index. 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) request = await self.async_api_call(url) @@ -351,12 +356,14 @@ class MyRadioAPI: source: str = self.config.get()["myradio_api_tracklist_source"] data = { "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. tracklist_id = None 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: self._logException("Failed to get tracklistid. {}".format(e)) @@ -370,7 +377,9 @@ class MyRadioAPI: self._log("Tracklistitemid is None, can't end tracklist.", WARNING) return False 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 self._log("Ending tracklistitemid {}".format(tracklistitemid)) diff --git a/helpers/normalisation.py b/helpers/normalisation.py index 900ffee..35f1d3b 100644 --- a/helpers/normalisation.py +++ b/helpers/normalisation.py @@ -1,49 +1,44 @@ 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. -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. def generate_normalised_file(filename: str): - if (not (isinstance(filename, str) and filename.endswith(".mp3"))): - raise ValueError("Invalid filename given.") + if not (isinstance(filename, str) and filename.endswith(".mp3")): + raise ValueError("Invalid filename given.") - # Already normalised. - if filename.endswith("-normalised.mp3"): - return filename + # Already normalised. + if filename.endswith("-normalised.mp3"): + 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. - if (os.path.exists(normalised_filename)): + # The file already exists, short circuit. + if os.path.exists(normalised_filename): + return normalised_filename + + sound = AudioSegment.from_file(filename, "mp3") + normalised_sound = effects.normalize(sound) + + normalised_sound.export(normalised_filename, bitrate="320k", format="mp3") return normalised_filename - sound = AudioSegment.from_file(filename, "mp3") - normalised_sound = effects.normalize(sound) #match_target_amplitude(sound, -10) - - normalised_sound.export(normalised_filename, bitrate="320k", format="mp3") - return normalised_filename # Returns either a normalised file path (based on filename), or the original if not available. -def get_normalised_filename_if_available(filename:str): - if (not (isinstance(filename, str) and filename.endswith(".mp3"))): - raise ValueError("Invalid filename given.") +def get_normalised_filename_if_available(filename: str): + if not (isinstance(filename, str) and filename.endswith(".mp3")): + raise ValueError("Invalid filename given.") - # Already normalised. - if filename.endswith("-normalised.mp3"): + # Already normalised. + if filename.endswith("-normalised.mp3"): + return filename + + normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3")) + + # normalised version exists + if os.path.exists(normalised_filename): + return normalised_filename + + # Else we've not got a normalised verison, just take original. return filename - - - normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3")) - - # normalised version exists - if (os.path.exists(normalised_filename)): - return normalised_filename - - # Else we've not got a normalised verison, just take original. - return filename - diff --git a/helpers/state_manager.py b/helpers/state_manager.py index 7c32a28..52e1321 100644 --- a/helpers/state_manager.py +++ b/helpers/state_manager.py @@ -1,6 +1,6 @@ import json import os -from logging import CRITICAL, DEBUG, INFO +from logging import DEBUG, INFO import time from datetime import datetime from copy import copy @@ -79,7 +79,7 @@ class StateManager: # If there are any new config options in the default state, save them. # Uses update() to save them to file too. 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]) except Exception: @@ -114,7 +114,6 @@ class StateManager: now = datetime.now() - current_time = now.strftime("%H:%M:%S") state_to_json["last_updated"] = current_time @@ -154,20 +153,30 @@ class StateManager: allow = False # 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 # If the two objects have dict representations, and they don't match, allow writing. # 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__: allow = True if not allow: # Just some debug logging. - if update_file and (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) + if update_file and ( + 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. # In this case, ignore the update @@ -176,11 +185,21 @@ class StateManager: if index > -1 and key in state_to_update: 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 list_items = state_to_update[key] 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 list_items[index] = value state_to_update[key] = list_items @@ -190,7 +209,12 @@ class StateManager: self.state = state_to_update 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. # Update the file self.write_to_file(state_to_update) diff --git a/launch.py b/launch.py index 859b7c7..dcf8088 100755 --- a/launch.py +++ b/launch.py @@ -70,7 +70,11 @@ if __name__ == "__main__": if sys.argv[1] == "Presenter": webbrowser.open("http://localhost:13500/presenter/") 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(0) diff --git a/package.py b/package.py index 18a9dfc..1689140 100644 --- a/package.py +++ b/package.py @@ -15,9 +15,10 @@ with open(resolve_local_file_path("package.json")) as file: build_beta = True try: import build + build_commit = build.BUILD build_branch = build.BRANCH - build_beta = (build_branch != "release") + build_beta = build_branch != "release" except (ModuleNotFoundError, AttributeError): pass BUILD: str = build_commit diff --git a/player.py b/player.py index 48a2c11..cd2f29d 100644 --- a/player.py +++ b/player.py @@ -21,6 +21,7 @@ # Stop the Pygame Hello message. import os + os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" 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. if not self._isLoaded(short_test=True): 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 def status(self): @@ -251,7 +255,9 @@ class Player: return False return True 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. if pos > 0: 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. # 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. - item.timeslotitemid = "GHOST-{}-{}".format(self.state.get()["channel"], time.time_ns()) + item.timeslotitemid = "GHOST-{}-{}".format( + self.state.get()["channel"], time.time_ns() + ) return item # 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) - loaded_item = self.state.get()["loaded_item"] if loaded_item: @@ -346,13 +353,15 @@ class Player: def remove_from_plan(self, weight: int) -> bool: plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"]) - found: Optional[PlanItem ] = None + found: Optional[PlanItem] = None before = [] for item in plan_copy: 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: if i.weight == weight: @@ -372,21 +381,19 @@ class Player: # So we'll want to update the weight. # We're removing the loaded item from the channel. - #if loaded_item.weight == weight: - loaded_item.weight = -1 - - + # if loaded_item.weight == weight: + loaded_item.weight = -1 # If loaded_item wasn't the same instance, we'd want to do the below. # We removed an item above it. Shift it up. - #elif loaded_item.weight > weight: + # elif loaded_item.weight > weight: # loaded_item.weight -= 1 # Else, new weight stays the same. - #else: + # else: # return True - self.state.update("loaded_item", loaded_item) + self.state.update("loaded_item", loaded_item) return True return False @@ -399,7 +406,10 @@ class Player: loaded_state = self.state.get() 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"]) showplan = loaded_state["show_plan"] @@ -412,14 +422,12 @@ class Player: break if loaded_item is None: - self.logger.log.error( - "Failed to find weight: {}".format(weight)) + self.logger.log.error("Failed to find weight: {}".format(weight)) return False reload = False if loaded_item.filename == "" or loaded_item.filename is None: - self.logger.log.info( - "Filename is not specified, loading from API.") + self.logger.log.info("Filename is not specified, loading from API.") reload = True elif not os.path.exists(loaded_item.filename): self.logger.log.warn( @@ -434,7 +442,9 @@ class Player: return False # 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) @@ -452,8 +462,7 @@ class Player: while load_attempt < 5: load_attempt += 1 try: - self.logger.log.info("Loading file: " + - str(loaded_item.filename)) + self.logger.log.info("Loading file: " + str(loaded_item.filename)) mixer.music.load(loaded_item.filename) except Exception: # We couldn't load that file. @@ -461,12 +470,14 @@ class Player: "Couldn't load file: " + str(loaded_item.filename) ) time.sleep(1) - continue # Try loading again. + continue # Try loading again. 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) - continue # Try loading again. + continue # Try loading again. try: if loaded_item.filename.endswith(".mp3"): @@ -475,14 +486,13 @@ class Player: else: # WARNING! Pygame / SDL can't seek .wav files :/ self.state.update( - "length", mixer.Sound( - loaded_item.filename).get_length() / 1000 + "length", + mixer.Sound(loaded_item.filename).get_length() / 1000, ) except Exception: - self.logger.log.exception( - "Failed to update the length of item.") + self.logger.log.exception("Failed to update the length of item.") time.sleep(1) - continue # Try loading again. + continue # Try loading again. # Everything worked, we made it! if loaded_item.cue > 0: @@ -561,7 +571,11 @@ class Player: try: marker = Marker(marker_str) 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 if timeslotitemid == "-1": @@ -569,10 +583,12 @@ class Player: if not self.isLoaded: return False 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 - plan_copy: List[PlanItem] = copy.copy(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: 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 if set_loaded: 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: 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 return success @@ -605,7 +629,9 @@ class Player: item.play_count_increment() if played else item.play_count_reset() self.state.update("show_plan", plan) 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) else: return False @@ -617,11 +643,10 @@ class Player: self.state.update("live", live) # If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing. - if (live): + if live: self._potentially_tracklist() return True - # Helper functions # 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"] time: int = -1 - if mode in ["on","fader-live"]: + if mode in ["on", "fader-live"]: time = 1 # Let's do it pretty quickly. 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.) time = TRACKLISTING_DELAYED_S 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.start() 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. def _potentially_end_tracklist(self): @@ -663,24 +694,34 @@ class Player: self.logger.log.info("No tracklist to end.") 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: - 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: - 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 self.state.update("tracklist_id", None) # 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.start() 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): state = self.state.get() loaded_item = state["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: self.logger.log.info("Not tracklisting since not playing.") @@ -688,20 +729,27 @@ class Player: else: tracklist_id = state["tracklist_id"] - if (not tracklist_id): - if (state["tracklist_mode"] == "fader-live" and not state["live"]): + if not tracklist_id: + if state["tracklist_mode"] == "fader-live" and not state["live"]: self.logger.log.info("Not tracklisting since fader is not live.") 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) 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: self.logger.log.info("Tracklist id: '{}'".format(tracklist_id)) self.state.update("tracklist_id", tracklist_id) else: - self.logger.log.info("Not tracklisting item '{}', already got tracklistid: '{}'".format( - loaded_item.name, tracklist_id)) + self.logger.log.info( + "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. self.tracklist_start_timer = None @@ -709,10 +757,14 @@ class Player: def _tracklist_end(self, 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) 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 @@ -727,7 +779,11 @@ class Player: return # 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 # TODO ENUM @@ -742,19 +798,25 @@ class Player: # If it's been removed, weight will be -1. # Just stop in this case. 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: - 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 len(state["show_plan"]) > loaded_item.weight+1: - self.load(loaded_item.weight+1) + if len(state["show_plan"]) > loaded_item.weight + 1: + self.load(loaded_item.weight + 1) return # Repeat All (Jump to top again) # TODO ENUM elif state["repeat"] == "all": - self.load(0) # Jump to the top. + self.load(0) # Jump to the top. return # No automations, just stop playing. @@ -795,8 +857,7 @@ class Player: self.state.update( "remaining", - max(0, (self.state.get()["length"] - - self.state.get()["pos_true"])), + max(0, (self.state.get()["length"] - self.state.get()["pos_true"])), ) def _ping_times(self): @@ -832,17 +893,18 @@ class Player: response += "FAIL" 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. self.logger.log.debug(("Sending: {}".format(response))) self.out_q.put(response) 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): # TODO This is hacky - self._retMsg(str(self.status), okay_str=True, - custom_prefix="ALL:STATUS:") + self._retMsg(str(self.status), okay_str=True, custom_prefix="ALL:STATUS:") def _fix_and_update_weights(self, plan): def _sort_weight(e: PlanItem): @@ -854,7 +916,6 @@ class Player: self.logger.log.debug("Weights before fixing:\n{}".format(before)) - plan.sort(key=_sort_weight) # Sort into weighted order. sorted = [] @@ -874,7 +935,11 @@ class Player: self.state.update("show_plan", plan) 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) @@ -899,7 +964,9 @@ class Player: self.state.update("channel", channel) 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. plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"]) @@ -908,8 +975,7 @@ class Player: loaded_state = copy.copy(self.state.state) if loaded_state["output"]: - self.logger.log.info("Setting output to: " + - str(loaded_state["output"])) + self.logger.log.info("Setting output to: " + str(loaded_state["output"])) self.output(loaded_state["output"]) else: self.logger.log.info("Using default output device.") @@ -918,7 +984,7 @@ class Player: loaded_item = loaded_state["loaded_item"] if loaded_item: # 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. # 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 = "" self.logger.log.warn( - "Message from unknown sender source: {}".format( - source) + "Message from unknown sender source: {}".format(source) ) continue @@ -981,9 +1046,13 @@ class Player: # Unpause, so we don't jump to 0, we play from the current pos. "PLAY": lambda: self._retMsg(self.unpause()), "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()), - "STOP": lambda: self._retMsg(self.stop(user_initiated=True)), + "STOP": lambda: self._retMsg( + self.stop(user_initiated=True) + ), "SEEK": lambda: self._retMsg( self.seek(float(self.last_msg.split(":")[1])) ), @@ -1011,19 +1080,33 @@ class Player: "UNLOAD": lambda: self._retMsg(self.unload()), "ADD": lambda: self._retMsg( self.add_to_plan( - json.loads( - ":".join(self.last_msg.split(":")[1:])) + json.loads(":".join(self.last_msg.split(":")[1:])) ) ), "REMOVE": lambda: self._retMsg( - self.remove_from_plan( - int(self.last_msg.split(":")[1])) + self.remove_from_plan(int(self.last_msg.split(":")[1])) ), "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])), - "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")), + "SETMARKER": lambda: self._retMsg( + self.set_marker( + self.last_msg.split(":")[1], + 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] @@ -1051,8 +1134,7 @@ class Player: except SystemExit: self.logger.log.info("Received SystemExit") except Exception as e: - self.logger.log.exception( - "Received unexpected Exception: {}".format(e)) + self.logger.log.exception("Received unexpected Exception: {}".format(e)) self.logger.log.info("Quiting player " + str(channel)) self.quit() diff --git a/player_handler.py b/player_handler.py index 5f1446f..9802654 100644 --- a/player_handler.py +++ b/player_handler.py @@ -10,7 +10,9 @@ from helpers.the_terminator import Terminator class PlayerHandler: 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") process_title = "Player Handler" @@ -31,7 +33,6 @@ class PlayerHandler: if command == "GET_PLAN" or command == "STATUS": file_to_q[channel].put(message) - # TODO ENUM if source in ["ALL", "WEBSOCKET"]: websocket_to_q[channel].put(message) @@ -46,7 +47,6 @@ class PlayerHandler: sleep(0.02) except Exception as e: - self.logger.log.exception( - "Received unexpected exception: {}".format(e)) + self.logger.log.exception("Received unexpected exception: {}".format(e)) del self.logger _exit(0) diff --git a/server.py b/server.py index 62d6cbb..f49c0b8 100644 --- a/server.py +++ b/server.py @@ -110,28 +110,53 @@ class BAPSicleServer: terminator = Terminator() 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"]): # 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 - 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)) self.player[channel] = multiprocessing.Process( 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() - 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.") self.player_handler = multiprocessing.Process( 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() - 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.") self.file_manager = multiprocessing.Process( target=FileManager, @@ -139,24 +164,38 @@ class BAPSicleServer: ) 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.") 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() - 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.") self.webserver = multiprocessing.Process( target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state) ) 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.") 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() @@ -179,7 +218,9 @@ class BAPSicleServer: ProxyManager.register("StateManager", StateManager) manager = ProxyManager() 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") @@ -203,8 +244,16 @@ class BAPSicleServer: self.controller_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("The Server UI is available at http://{}:{}".format(self.state.get()["host"], self.state.get()["port"])) + print( + "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. if False: diff --git a/setup.py b/setup.py index b38ade3..9af4e1d 100644 --- a/setup.py +++ b/setup.py @@ -7,4 +7,5 @@ setup( description=package.DESCRIPTION, author=package.AUTHOR, license=package.LICENSE, - packages=find_packages()) + packages=find_packages(), +) diff --git a/tests/test_player.py b/tests/test_player.py index ccd6d9e..1767966 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -68,7 +68,9 @@ class TestPlayer(unittest.TestCase): @classmethod def setUpClass(cls): 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 # 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_to_q = multiprocessing.Queue() 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._send_msg_wait_OKAY("CLEAR") # Empty any previous track items. @@ -125,7 +128,7 @@ class TestPlayer(unittest.TestCase): source = response[: response.index(":")] if source in sources_filter: return response[ - len(source + ":" + msg) + 1: + len(source + ":" + msg) + 1 : ] # +1 to remove trailing : on source. except Empty: pass @@ -339,9 +342,13 @@ class TestPlayer(unittest.TestCase): # Now test that all the markers we setup are present. item = json_obj["show_plan"][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([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] self.assertEqual(item["weight"], 1) @@ -355,7 +362,9 @@ class TestPlayer(unittest.TestCase): self.assertEqual(item["intro"], 0.0) self.assertEqual(item["outro"], 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 diff --git a/web_server.py b/web_server.py index 7972170..363d7bc 100644 --- a/web_server.py +++ b/web_server.py @@ -16,7 +16,11 @@ from time import sleep import json 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.device_manager import DeviceManager 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.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_FILENAME = LOG_FILEPATH + "/WebServer.log" @@ -52,17 +59,17 @@ LOGGING_CONFIG = dict( "file": { "class": "logging.FileHandler", "formatter": "generic", - "filename": LOG_FILENAME + "filename": LOG_FILENAME, }, "error_file": { "class": "logging.FileHandler", "formatter": "generic", - "filename": LOG_FILENAME + "filename": LOG_FILENAME, }, "access_file": { "class": "logging.FileHandler", "formatter": "access", - "filename": LOG_FILENAME + "filename": LOG_FILENAME, }, }, formatters={ @@ -113,7 +120,7 @@ def ui_index(request): "server_build": config["server_build"], "server_name": config["server_name"], "server_beta": config["server_beta"], - "server_branch": config["server_branch"] + "server_branch": config["server_branch"], } return render_template("index.html", data=data) @@ -124,8 +131,7 @@ def ui_status(request): for i in range(server_state.get()["num_channels"]): channel_states.append(status(i)) - data = {"channels": channel_states, - "ui_page": "status", "ui_title": "Status"} + data = {"channels": channel_states, "ui_page": "status", "ui_title": "Status"} return render_template("status.html", data=data) @@ -153,7 +159,7 @@ def ui_config_server(request): "ui_title": "Server Config", "state": server_state.get(), "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) @@ -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_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")) return redirect("/restart") @@ -192,11 +200,7 @@ def ui_logs_list(request): log_files.append(file.rstrip(".log")) log_files.sort() - data = { - "ui_page": "logs", - "ui_title": "Logs", - "logs": log_files - } + data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files} 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)) 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_title": "Logs - {}".format(path), - "page": page + "page": page, } log_file.close() return render_template("log.html", data=data) @@ -296,6 +302,7 @@ def player_all_stop(request): # Show Plan Functions + @app.route("/plan/load/") def plan_load(request, timeslotid: int): @@ -314,6 +321,7 @@ def plan_clear(request): # API Proxy Endpoints + @app.route("/plan/list") async def api_list_showplans(request): @@ -323,7 +331,11 @@ async def api_list_showplans(request): @app.route("/library/search/track") 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/") @@ -368,7 +380,7 @@ def json_status(request): async def audio_file(request, type: str, id: int): if type not in ["managed", "track"]: 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. filename = get_normalised_filename_if_available(filename) @@ -378,18 +390,25 @@ async def audio_file(request, type: str, id: int): # 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") dist_directory = resolve_local_file_path("presenter-build") -app.static('/presenter', dist_directory) -app.static("/presenter/", resolve_local_file_path("presenter-build/index.html"), - strict_slashes=True, name="presenter-index") +app.static("/presenter", dist_directory) +app.static( + "/presenter/", + resolve_local_file_path("presenter-build/index.html"), + strict_slashes=True, + name="presenter-index", +) # Helper Functions + def status(channel: int): while not player_from_q[channel].empty(): player_from_q[channel].get() # Just waste any previous status responses. @@ -402,7 +421,7 @@ def status(channel: int): if response.startswith("UI:STATUS:"): response = response.split(":", 2)[2] # TODO: Handle OKAY / FAIL - response = response[response.index(":") + 1:] + response = response[response.index(":") + 1 :] try: response = json.loads(response) except Exception as e: @@ -416,6 +435,7 @@ def status(channel: int): sleep(0.02) + # WebServer Start / Stop Functions @@ -428,7 +448,7 @@ def quit(request): "ui_title": "Quitting BAPSicle", "title": "See you later!", "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) @@ -444,7 +464,7 @@ def restart(request): "ui_menu": False, "message": "Just putting BAPSicle back in the freezer for a moment!", "redirect_to": "/", - "redirect_wait_ms": 10000 + "redirect_wait_ms": 10000, } return render_template("message.html", data) @@ -467,13 +487,15 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana terminate = Terminator() while not terminate.terminate: try: - sync(app.run( - host=server_state.get()["host"], - port=server_state.get()["port"], - debug=(not isBundelled()), - auto_reload=False, - access_log=(not isBundelled()) - )) + sync( + app.run( + host=server_state.get()["host"], + port=server_state.get()["port"], + debug=(not isBundelled()), + auto_reload=False, + access_log=(not isBundelled()), + ) + ) except Exception: break loop = asyncio.get_event_loop() diff --git a/websocket_server.py b/websocket_server.py index 6fe3741..ca136f7 100644 --- a/websocket_server.py +++ b/websocket_server.py @@ -74,9 +74,7 @@ class WebsocketServer: self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket)) try: - self.threads = await shield( - asyncio.gather(self.from_webstudio) - ) + self.threads = await shield(asyncio.gather(self.from_webstudio)) finally: self.from_webstudio.cancel() @@ -92,13 +90,10 @@ class WebsocketServer: channel = int(data["channel"]) self.sendCommand(channel, data) - await asyncio.wait( - [conn.send(message) for conn in self.baps_clients] - ) + await asyncio.wait([conn.send(message) for conn in self.baps_clients]) except websockets.exceptions.ConnectionClosedError as e: - self.logger.log.error( - "Client Disconncted {}, {}".format(websocket, e)) + self.logger.log.error("Client Disconncted {}, {}".format(websocket, e)) except Exception as e: self.logger.log.exception( @@ -152,8 +147,7 @@ class WebsocketServer: extra += str(data["timeslotId"]) elif command == "SETMARKER": extra += "{}:{}".format( - data["timeslotitemid"], - json.dumps(data["marker"]) + data["timeslotitemid"], json.dumps(data["marker"]) ) # TODO: Move this to player handler. @@ -174,21 +168,22 @@ class WebsocketServer: # Now send the special case. self.channel_to_q[new_channel].put( - "WEBSOCKET:ADD:" + json.dumps(item)) + "WEBSOCKET:ADD:" + json.dumps(item) + ) # Don't bother, we should be done. return except ValueError as e: self.logger.log.exception( - "Error decoding extra data {} for command {} ".format( - e, command - ) + "Error decoding extra data {} for command {} ".format(e, command) ) pass # 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 != "": message += ":" + extra @@ -202,9 +197,7 @@ class WebsocketServer: ) else: - self.logger.log.error( - "Command missing from message. Data: {}".format(data) - ) + self.logger.log.error("Command missing from message. Data: {}".format(data)) async def handle_to_webstudio(self): @@ -244,9 +237,7 @@ class WebsocketServer: data = json.dumps( {"command": command, "data": message, "channel": channel} ) - await asyncio.wait( - [conn.send(data) for conn in self.baps_clients] - ) + await asyncio.wait([conn.send(data) for conn in self.baps_clients]) except queue.Empty: continue except ValueError: