Ejemplo n.º 1
0
    def __init__(self, server_to_q: List[Queue], server_from_q: List[Queue],
                 state: StateManager):

        process_title = "ControllerHandler"
        setproctitle(process_title)
        current_process().name = process_title

        self.ser = None
        self.logger = LoggingManager("ControllerMattchBox")

        self.server_state = state  # This is a copy, will not update :/

        # This doesn't run, the callback function gets lost
        # due to state being a copy in the multiprocessing process.
        # self.server_state.add_callback(self._state_handler)

        # Allow server config changes to trigger controller reload if required.
        self.port = None
        self.next_port = self.server_state.get()["serial_port"]
        self.logger.log.info("Server config gives port as: {}".format(
            self.next_port))

        self.server_from_q = server_from_q
        self.server_to_q = server_to_q

        self.handler()
Ejemplo n.º 2
0
    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"
        setproctitle(process_title)
        current_process().name = process_title

        terminator = Terminator()
        try:
            while not terminator.terminate:

                for channel in range(len(channel_from_q)):
                    try:
                        message = channel_from_q[channel].get_nowait()
                        source = message.split(":")[0]
                        command = message.split(":")[1]

                        # Let the file manager manage the files based on status and loading new show plan triggers.
                        if command == "GETPLAN" or command == "STATUS":
                            file_to_q[channel].put(message)

                        # TODO ENUM
                        if source in ["ALL", "WEBSOCKET"]:
                            websocket_to_q[channel].put(message)
                        if source in ["ALL", "UI"]:
                            if not message.split(":")[1] == "POS":
                                # We don't care about position update spam
                                ui_to_q[channel].put(message)
                        if source in ["ALL", "CONTROLLER"]:
                            controller_to_q[channel].put(message)
                    except Exception:
                        pass

                sleep(0.02)
        except Exception as e:
            self.logger.log.exception(
                "Received unexpected exception: {}".format(e))
        del self.logger
        _exit(0)
Ejemplo n.º 3
0
def WebServer(player_to: List[Queue], player_from: List[Queue],
              state: StateManager):

    global player_to_q, player_from_q, server_state, api, app, alerts
    player_to_q = player_to
    player_from_q = player_from
    server_state = state

    logger = LoggingManager("WebServer")
    api = MyRadioAPI(logger, state)
    alerts = AlertManager()

    process_title = "Web Server"
    setproctitle(process_title)
    CORS(app, supports_credentials=True)  # Allow ALL CORS!!!

    terminate = Terminator()
    while not terminate.terminate:
        try:
            sync(
                app.run(
                    host=server_state.get()["host"],
                    port=server_state.get()["port"],
                    auto_reload=False,
                    debug=not package.BETA,
                    access_log=not package.BETA,
                ))
        except Exception:
            break
    try:
        loop = asyncio.get_event_loop()
        if loop:
            loop.close()
        if app:
            app.stop()
            del app
    except Exception:
        pass
    def __init__(self, in_q, out_q, state):

        self.channel_to_q = in_q
        self.webstudio_to_q = out_q

        process_title = "Websockets Servr"
        setproctitle(process_title)
        current_process().name = process_title

        self.logger = LoggingManager("Websockets")
        self.server_name = state.get()["server_name"]

        self.websocket_server = serve(self.websocket_handler,
                                      state.get()["host"],
                                      state.get()["ws_port"])

        asyncio.get_event_loop().run_until_complete(self.websocket_server)
        asyncio.get_event_loop().run_until_complete(self.handle_to_webstudio())

        try:
            asyncio.get_event_loop().run_forever()
        except Exception:
            # Sever died somehow, just quit out.
            self.quit()
Ejemplo n.º 5
0
    def __init__(self, channel_from_q: List[Queue], server_config: StateManager):

        self.logger = LoggingManager("FileManager")
        self.api = MyRadioAPI(self.logger, server_config)

        process_title = "File Manager"
        setproctitle(process_title)
        current_process().name = process_title

        terminator = Terminator()

        self.normalisation_mode = server_config.get()["normalisation_mode"]

        if self.normalisation_mode != "on":
            self.logger.log.info("Normalisation is disabled.")
        else:
            self.logger.log.info("Normalisation is enabled.")

        self.channel_count = len(channel_from_q)
        self.channel_received = None
        self.last_known_show_plan = [[]] * self.channel_count
        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
        try:

            while not terminator.terminate:
                # If all channels have received the delete command, reset for the next one.
                if (
                    self.channel_received is 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

                    try:
                        # 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] is False
                            ):
                                # 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
                            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

                            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

                    except Exception:
                        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)

        except Exception as e:
            self.logger.log.exception(
                "Received unexpected exception: {}".format(e))
        del self.logger
Ejemplo n.º 6
0
 def setUpClass(cls):
     cls.logger = LoggingManager("Test_Player")
     cls.server_state = StateManager(
         "BAPSicleServer",
         cls.logger,
         default_state={"tracklist_mode": "off"})  # Mostly dummy here.
Ejemplo n.º 7
0
    def __init__(
        self,
        channel: int,
        in_q: multiprocessing.Queue,
        out_q: multiprocessing.Queue,
        server_state: StateManager,
    ):

        process_title = "Player: Channel " + str(channel)
        setproctitle.setproctitle(process_title)
        multiprocessing.current_process().name = process_title

        self.running = True
        self.out_q = out_q

        self.logger = LoggingManager(
            "Player" + str(channel), debug=package.BETA)

        self.api = MyRadioAPI(self.logger, server_state)

        self.state = StateManager(
            "Player" + str(channel),
            self.logger,
            self.__default_state,
            self.__rate_limited_params,
        )

        self.state.update("start_time", datetime.now().timestamp())

        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(
            "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"])
        self._fix_and_update_weights(plan_copy)

        loaded_state = copy.copy(self.state.state)

        if 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.")
            self.output()

        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)

            # 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 loaded_state["pos_true"] != 0:
                self.logger.log.info(
                    "Seeking to pos_true: " + str(loaded_state["pos_true"])
                )
                try:
                    self.seek(loaded_state["pos_true"])
                except error:
                    self.logger.log.error("Failed to seek on player start. Continuing anyway.")

            if loaded_state["playing"] is True:
                self.logger.log.info("Resuming playback on init.")
                # Use un-pause as we don't want to jump to a new position.
                try:
                    self.unpause()
                except error:
                    self.logger.log.error("Failed to unpause on player start. Continuing anyway.")
        else:
            self.logger.log.info("No file was previously loaded to resume.")

        try:
            while self.running:
                self._updateState()
                self._ping_times()
                try:
                    message = in_q.get_nowait()
                    source = message.split(":")[0]
                    if source not in VALID_MESSAGE_SOURCES:
                        self.last_msg_source = ""
                        self.last_msg = ""
                        self.logger.log.warn(
                            "Message from unknown sender source: {}".format(
                                source)
                        )
                        continue

                    self.last_msg_source = source
                    self.last_msg = message.split(":", 1)[1]

                    self.logger.log.debug(
                        "Recieved message from source {}: {}".format(
                            self.last_msg_source, self.last_msg
                        )
                    )
                except Empty:
                    # The incomming message queue was empty,
                    # skip message processing

                    # If we're getting no messages, sleep.
                    # But if we do have messages, once we've done with one, we'll check for the next one more quickly.
                    time.sleep(0.05)
                else:

                    # We got a message.

                    ## Check if we're successfully loaded
                    # This is here so that we can check often, but not every single loop
                    # Only when user gives input.
                    self._checkIsLoaded()

                    # Output re-inits the mixer, so we can do this any time.
                    if self.last_msg.startswith("OUTPUT"):
                        split = self.last_msg.split(":")
                        self._retMsg(self.output(split[1]))

                    elif self.isInit:
                        message_types: Dict[
                            str, Callable[..., Any]
                        ] = {  # TODO Check Types
                            "STATUS": lambda: self._retMsg(self.status, True),
                            # Audio Playout
                            # 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.
                            "UNPAUSE": lambda: self._retMsg(self.unpause()),
                            "STOP": lambda: self._retMsg(
                                self.stop(user_initiated=True)
                            ),
                            "SEEK": lambda: self._retMsg(
                                self.seek(float(self.last_msg.split(":")[1]))
                            ),
                            "AUTOADVANCE": lambda: self._retMsg(
                                self.set_auto_advance(
                                    (self.last_msg.split(":")[1] == "True")
                                )
                            ),
                            "REPEAT": lambda: self._retMsg(
                                self.set_repeat(self.last_msg.split(":")[1])
                            ),
                            "PLAYONLOAD": lambda: self._retMsg(
                                self.set_play_on_load(
                                    (self.last_msg.split(":")[1] == "True")
                                )
                            ),
                            # Show Plan Items
                            "GETPLAN": lambda: self._retMsg(
                                self.get_plan(int(self.last_msg.split(":")[1]))
                            ),
                            "LOAD": lambda: self._retMsg(
                                self.load(int(self.last_msg.split(":")[1]))
                            ),
                            "LOADED?": lambda: self._retMsg(self.isLoaded),
                            "UNLOAD": lambda: self._retMsg(self.unload()),
                            "ADD": lambda: self._retMsg(
                                self.add_to_plan(
                                    json.loads(
                                        ":".join(self.last_msg.split(":")[1:]))
                                )
                            ),
                            "REMOVE": lambda: self._retMsg(
                                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")
                            ),
                        }

                        message_type: str = self.last_msg.split(":")[0]

                        if message_type in message_types.keys():
                            message_types[message_type]()

                        elif self.last_msg == "QUIT":
                            self._retMsg(True)
                            self.running = False
                            continue

                        else:
                            self._retMsg("Unknown Command")
                    else:

                        if self.last_msg == "STATUS":
                            self._retMsg(self.status)
                        else:
                            self._retMsg(False)

        # Catch the player being killed externally.
        except KeyboardInterrupt:
            self.logger.log.info("Received KeyboardInterupt")
        except SystemExit:
            self.logger.log.info("Received SystemExit")
        except Exception as e:
            self.logger.log.exception(
                "Received unexpected Exception: {}".format(e))

        self.logger.log.info("Quiting player " + str(channel))
        self.quit()
        self._retAll("QUIT")
        del self.logger
        os._exit(0)
Ejemplo n.º 8
0
    def startServer(self):
        # On MacOS, the default causes something to keep creating new processes.
        # On Linux, this is needed to make pulseaudio initiate properly.
        if isMacOS() or isLinux():
            multiprocessing.set_start_method("spawn", True)

        process_title = "startServer"
        setproctitle(process_title)
        multiprocessing.current_process().name = process_title

        self.logger = LoggingManager("BAPSicleServer")

        # Since we're passing the StateManager across processes, it must be made a manager.
        # PLEASE NOTE: You can't read attributes directly, use state.get()["var"] and state.update("var", "val")
        ProxyManager.register("StateManager", StateManager)
        manager = ProxyManager()
        manager.start()
        self.state: StateManager = manager.StateManager(
            "BAPSicleServer", self.logger, self.default_state)

        self.state.update("running_state", "running")
        self.state.update("start_time", datetime.now().timestamp())

        print("Launching BAPSicle...")

        # TODO: Check these match, if not, trigger any upgrade noticies / welcome
        self.state.update("server_version", package.VERSION)
        self.state.update("server_build", package.BUILD)
        self.state.update("server_branch", package.BRANCH)
        self.state.update("server_beta", package.BETA)

        channel_count = self.state.get()["num_channels"]
        self.player = [None] * channel_count

        for channel in range(self.state.get()["num_channels"]):

            self.player_to_q.append(multiprocessing.Queue())
            self.player_from_q.append(multiprocessing.Queue())
            self.ui_to_q.append(multiprocessing.Queue())
            self.websocket_to_q.append(multiprocessing.Queue())
            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"]))

        # TODO Move this to player or installer.
        if False:
            if not isMacOS():

                # Temporary RIP.

                # Welcome Speech

                text_to_speach = pyttsx3.init()
                text_to_speach.save_to_file(
                    """Thank-you for installing BAPSicle - the play-out server from the broadcasting and presenting suite.
                By default, this server is accepting connections on port 13500
                The version of the server service is {}
                Please refer to the documentation included with this application for further assistance."""
                    .format(package.VERSION),
                    "dev/welcome.mp3",
                )
                text_to_speach.runAndWait()

                new_item: Dict[str, Any] = {
                    "channel_weight": 0,
                    "filename": "dev/welcome.mp3",
                    "title": "Welcome to BAPSicle",
                    "artist": "University Radio York",
                }

                self.player_to_q[0].put("ADD:" + json.dumps(new_item))
                self.player_to_q[0].put("LOAD:0")
                self.player_to_q[0].put("PLAY")