diff --git a/build/requirements-dev.txt b/build/requirements-dev.txt new file mode 100644 index 0000000..45c47db --- /dev/null +++ b/build/requirements-dev.txt @@ -0,0 +1,3 @@ +flake8 +black +autopep8 diff --git a/build/requirements.txt b/build/requirements.txt index 664cc26..84c18be 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,4 +1,4 @@ -autopep8 +wheel pygame==2.0.1 sanic==21.3.4 sanic-Cors==1.0.0 diff --git a/file_manager.py b/file_manager.py index a6cca0f..cb99c9b 100644 --- a/file_manager.py +++ b/file_manager.py @@ -41,7 +41,7 @@ class FileManager: while not terminator.terminate: # If all channels have received the delete command, reset for the next one. if ( - self.channel_received == None + self.channel_received is None or self.channel_received == [True] * self.channel_count ): self.channel_received = [False] * self.channel_count @@ -60,18 +60,22 @@ class FileManager: if command == "GETPLAN": if ( - self.channel_received != [False] * self.channel_count + 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 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/") + # 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( @@ -102,7 +106,8 @@ class FileManager: ) continue 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 @@ -142,7 +147,8 @@ class FileManager: 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. @@ -169,7 +175,8 @@ class FileManager: ) ) - # 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) ) @@ -181,7 +188,8 @@ class FileManager: if did_download: downloaded_something = True self.logger.log.info( - "File successfully preloaded: {}".format(item_obj.filename) + "File successfully preloaded: {}".format( + item_obj.filename) ) break else: @@ -189,7 +197,8 @@ class FileManager: # 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. + # 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 @@ -241,7 +250,8 @@ class FileManager: 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)) + self.logger.log.exception( + "Failed to generate normalised file.", str(e)) continue self.known_channels_normalised[channel] = not normalised_something diff --git a/launch.py b/launch.py index dcf8088..5e2ac64 100755 --- a/launch.py +++ b/launch.py @@ -39,7 +39,8 @@ def startServer(notifications=False): # Catch the handler being killed externally. except Exception as e: - printer("Received Exception {} with args: {}".format(type(e).__name__, e.args)) + printer("Received Exception {} with args: {}".format( + type(e).__name__, e.args)) if server and server.is_alive(): server.terminate() server.join() diff --git a/package.json b/package.json index 27ffe9f..9b38633 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "presenter-install": "cd presenter && git submodule update --init && yarn --network-timeout 100000", "presenter-make": "npm run presenter-install && (rm -r presenter-build || true) && cd presenter && yarn build-baps && cp -r build ../presenter-build && cd ../ && npm install", "test": "echo \"Error: no test specified\" && exit 1", - "presenter-start": "cd presenter && yarn start" + "presenter-start": "cd presenter && yarn start", + "lint": "autopep8 -r ./*.py -a -a --ignore E402,E226,E24,W50,W690 --max-line-length 127 --in-place" }, "repository": { "type": "git", diff --git a/player.py b/player.py index cd2f29d..bcacec9 100644 --- a/player.py +++ b/player.py @@ -20,29 +20,28 @@ # that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting. # Stop the Pygame Hello message. +import package +from baps_types.marker import Marker +from baps_types.plan import PlanItem +from helpers.logging_manager import LoggingManager +from helpers.state_manager import StateManager +from helpers.myradio_api import MyRadioAPI +from helpers.normalisation import get_normalised_filename_if_available +from threading import Timer +from syncer import sync +from mutagen.mp3 import MP3 +from pygame import mixer +from typing import Any, Callable, Dict, List, Optional +import time +import json +import copy +import setproctitle +import multiprocessing +from queue import Empty import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" -from queue import Empty -import multiprocessing -import setproctitle -import copy -import json -import time -from typing import Any, Callable, Dict, List, Optional -from pygame import mixer -from mutagen.mp3 import MP3 -from syncer import sync -from threading import Timer - -from helpers.normalisation import get_normalised_filename_if_available -from helpers.myradio_api import MyRadioAPI -from helpers.state_manager import StateManager -from helpers.logging_manager import LoggingManager -from baps_types.plan import PlanItem -from baps_types.marker import Marker -import package # TODO ENUM VALID_MESSAGE_SOURCES = ["WEBSOCKET", "UI", "CONTROLLER", "TEST", "ALL"] @@ -329,25 +328,28 @@ class Player: # Right. So this may be confusing. # So... If the user has just moved the loaded item in the channel (by removing above and readding) # Then we want to re-associate the loaded_item object reference with the new one. - # The loaded item object before this change is now an ophan, which was kept around while the loaded item was potentially moved to another channel. + # The loaded item object before this change is now an ophan, which was + # kept around while the loaded item was potentially moved to another + # channel. if loaded_item.timeslotitemid == new_item_obj.timeslotitemid: self.state.update("loaded_item", new_item_obj) # NOPE NOPE NOPE # THIS IS AN EXAMPLE OF WHAT NOT TO DO! - # ONCE AGAIN, THE LOADED ITEM IS THE SAME OBJECT INSTANCE AS THE ONE IN THE SHOW PLAN (AS LONG AS IT HASN'T BEEN RE/MOVED) + # ONCE AGAIN, THE LOADED ITEM IS THE SAME OBJECT INSTANCE AS THE ONE IN + # THE SHOW PLAN (AS LONG AS IT HASN'T BEEN RE/MOVED) - ## loaded_item.weight = new_item_obj.weight + # loaded_item.weight = new_item_obj.weight # Bump the loaded_item's weight if we just added a new item above it. - ##elif loaded_item.weight >= new_item_obj.weight: - ## loaded_item.weight += 1 + # elif loaded_item.weight >= new_item_obj.weight: + # loaded_item.weight += 1 # Else, new weight stays the same. - ##else: - ## return True + # else: + # return True - ##self.state.update("loaded_item", loaded_item) + # self.state.update("loaded_item", loaded_item) return True @@ -422,12 +424,14 @@ 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( @@ -436,7 +440,8 @@ class Player: reload = True if reload: - loaded_item.filename = sync(self.api.get_filename(item=loaded_item)) + loaded_item.filename = sync( + self.api.get_filename(item=loaded_item)) if not loaded_item.filename: return False @@ -462,7 +467,8 @@ 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. @@ -487,10 +493,12 @@ class Player: # WARNING! Pygame / SDL can't seek .wav files :/ self.state.update( "length", - mixer.Sound(loaded_item.filename).get_length() / 1000, + 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. @@ -505,7 +513,8 @@ class Player: return True - self.logger.log.error("Failed to load track after numerous retries.") + self.logger.log.error( + "Failed to load track after numerous retries.") return False return False @@ -610,7 +619,8 @@ class Player: if set_loaded: try: self.state.update( - "loaded_item", self.state.get()["loaded_item"].set_marker(marker) + "loaded_item", self.state.get( + )["loaded_item"].set_marker(marker) ) except Exception as e: self.logger.log.error( @@ -677,7 +687,8 @@ class Player: def _potentially_end_tracklist(self): if self.tracklist_start_timer: - self.logger.log.info("A tracklist start timer was running, cancelling.") + self.logger.log.info( + "A tracklist start timer was running, cancelling.") self.tracklist_start_timer.cancel() self.tracklist_start_timer = None @@ -708,7 +719,8 @@ class Player: 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 = Timer( + 1, self._tracklist_end, [tracklist_id]) self.tracklist_end_timer.start() else: self.logger.log.warning( @@ -731,7 +743,8 @@ class Player: tracklist_id = state["tracklist_id"] 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.") + self.logger.log.info( + "Not tracklisting since fader is not live.") else: self.logger.log.info( "Tracklisting item: '{}'".format(loaded_item.name) @@ -742,7 +755,8 @@ class Player: "Failed to tracklist '{}'".format(loaded_item.name) ) 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) else: self.logger.log.info( @@ -857,7 +871,8 @@ 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): @@ -904,7 +919,8 @@ class Player: 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): @@ -949,7 +965,8 @@ class Player: self.running = True self.out_q = out_q - self.logger = LoggingManager("Player" + str(channel), debug=package.build_beta) + self.logger = LoggingManager( + "Player" + str(channel), debug=package.build_beta) self.api = MyRadioAPI(self.logger, server_state) @@ -963,7 +980,8 @@ class Player: self.state.add_callback(self._send_status) 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. @@ -975,7 +993,8 @@ 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.") @@ -996,7 +1015,8 @@ class Player: if loaded_state["playing"] is True: self.logger.log.info("Resuming playback on init.") - self.unpause() # Use un-pause as we don't want to jump to a new position. + # Use un-pause as we don't want to jump to a new position. + self.unpause() else: self.logger.log.info("No file was previously loaded to resume.") @@ -1012,7 +1032,8 @@ 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 @@ -1080,11 +1101,13 @@ 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( @@ -1105,7 +1128,8 @@ class Player: ) ), "SETLIVE": lambda: self._retMsg( - self.set_live(self.last_msg.split(":")[1] == "True") + self.set_live( + self.last_msg.split(":")[1] == "True") ), } @@ -1134,7 +1158,8 @@ 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 9802654..da17b64 100644 --- a/player_handler.py +++ b/player_handler.py @@ -47,6 +47,7 @@ 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 f49c0b8..eae1d52 100644 --- a/server.py +++ b/server.py @@ -22,15 +22,12 @@ import json from setproctitle import setproctitle import psutil -from helpers.os_environment import isBundelled, isMacOS +from helpers.os_environment import isMacOS if not isMacOS(): # Rip, this doesn't like threading on MacOS. import pyttsx3 -if isBundelled(): - import build - import package from typing import Dict, List from helpers.state_manager import StateManager @@ -111,7 +108,8 @@ class BAPSicleServer: log_function = self.logger.log.info while ( - not terminator.terminate and self.state.get()["running_state"] == "running" + not terminator.terminate and self.state.get()[ + "running_state"] == "running" ): for channel in range(self.state.get()["num_channels"]): @@ -122,7 +120,8 @@ class BAPSicleServer: 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( target=player.Player, args=( @@ -183,7 +182,8 @@ class BAPSicleServer: ): log_function("Webserver not running, (re)starting.") 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() @@ -320,7 +320,8 @@ class BAPSicleServer: self.player_handler.join(timeout=PROCESS_KILL_TIMEOUT_S) del self.player_handler - # Now we've stopped everything else, now is the time to stop the players. This is to keep playing for as long as possible during a restart. + # Now we've stopped everything else, now is the time to stop the players. + # This is to keep playing for as long as possible during a restart. print("Stopping Players") for q in self.player_to_q: q.put("ALL:QUIT") diff --git a/web_server.py b/web_server.py index 363d7bc..90f6527 100644 --- a/web_server.py +++ b/web_server.py @@ -1,4 +1,4 @@ -from sanic import Sanic, log +from sanic import Sanic from sanic.exceptions import NotFound, abort from sanic.response import html, file, redirect from sanic.response import json as resp_json @@ -131,7 +131,8 @@ 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) @@ -175,16 +176,20 @@ def ui_config_server_update(request): server_state.update("ws_port", int(request.form.get("ws_port"))) serial_port = request.form.get("serial_port") - server_state.update("serial_port", None if serial_port == "None" else serial_port) + server_state.update("serial_port", None if serial_port == + "None" else serial_port) # Because we're not showing the api key once it's set. if "myradio_api_key" in request.form and request.form.get("myradio_api_key") != "": - server_state.update("myradio_api_key", request.form.get("myradio_api_key")) + server_state.update("myradio_api_key", + request.form.get("myradio_api_key")) - 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_tracklist_source", request.form.get("myradio_api_tracklist_source") + "myradio_api_tracklist_source", request.form.get( + "myradio_api_tracklist_source") ) server_state.update("tracklist_mode", request.form.get("tracklist_mode")) @@ -195,9 +200,9 @@ def ui_config_server_update(request): def ui_logs_list(request): files = os.listdir(resolve_external_file_path("/logs")) log_files = [] - for file in files: - if file.endswith(".log"): - log_files.append(file.rstrip(".log")) + for file_name in files: + if file_name.endswith(".log"): + log_files.append(file_name.rstrip(".log")) log_files.sort() data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files} @@ -215,7 +220,7 @@ 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) + -300 * page:(-300 * (page - 1) if page > 1 else None) ][::-1], "ui_page": "logs", "ui_title": "Logs - {}".format(path), @@ -380,7 +385,8 @@ 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) @@ -411,7 +417,8 @@ app.static( def status(channel: int): while not player_from_q[channel].empty(): - player_from_q[channel].get() # Just waste any previous status responses. + # Just waste any previous status responses. + player_from_q[channel].get() player_to_q[channel].put("UI:STATUS") retries = 0 @@ -421,7 +428,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: diff --git a/websocket_server.py b/websocket_server.py index ca136f7..e43ecb3 100644 --- a/websocket_server.py +++ b/websocket_server.py @@ -71,7 +71,8 @@ class WebsocketServer: for channel in self.channel_to_q: channel.put("WEBSOCKET:STATUS") - self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket)) + self.from_webstudio = asyncio.create_task( + self.handle_from_webstudio(websocket)) try: self.threads = await shield(asyncio.gather(self.from_webstudio)) @@ -93,7 +94,8 @@ class WebsocketServer: 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( @@ -176,13 +178,15 @@ class WebsocketServer: 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. + # Put the command in at the end, in case MOVE etc changed it. + command ) if extra != "": message += ":" + extra @@ -197,7 +201,8 @@ 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):