From 6718ddcf2d7ec86ad253972fcc2624b068eca6db Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 10 Apr 2021 21:30:41 +0100 Subject: [PATCH 1/3] Add markers test. --- tests/test_player.py | 70 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/tests/test_player.py b/tests/test_player.py index 817284f..c5ac8ae 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -1,4 +1,4 @@ -from helpers.os_environment import isMacOS +from typing import Optional from queue import Empty import unittest import multiprocessing @@ -8,6 +8,7 @@ import json from player import Player from helpers.logging_manager import LoggingManager +from helpers.os_environment import isMacOS # How long to wait (by default) in secs for the player to respond. TIMEOUT_MSG_MAX_S = 10 @@ -34,7 +35,23 @@ def getPlanItem(length: int, weight: int): def getPlanItemJSON(length: int, weight: int): - return str(json.dumps(getPlanItem(length, weight))) + return str(json.dumps(getPlanItem(**locals()))) + + +# All because constant dicts are still mutable in python :/ +def getMarker(name: str, time: float, position: str, section: Optional[str] = None): + # Time is not validated here, to allow tests to check server response. + marker = { + "name": name, # User friendly name, eg. "Hit the vocals" + "time": time, # Position (secs) through item + "section": section, # for linking in loops, if none, assume intro, cue, outro based on "position" + "position": position, # start, mid, end + } + return marker + + +def getMarkerJSON(name: str, time: float, position: str, section: Optional[str] = None): + return str(json.dumps(getMarker(**locals()))) class TestPlayer(unittest.TestCase): @@ -283,6 +300,55 @@ class TestPlayer(unittest.TestCase): time.sleep(5) + # TODO: Test validation of trying to break this. + # TODO: Test cue behaviour. + def test_markers(self): + self._send_msg_wait_OKAY("ADD:" + getPlanItemJSON(5, 0)) + self._send_msg_wait_OKAY("ADD:" + getPlanItemJSON(5, 1)) + self._send_msg_wait_OKAY("ADD:" + getPlanItemJSON(5, 2)) + + self._send_msg_wait_OKAY("LOAD:2") # To test currently loaded marker sets. + + markers = [ + getMarkerJSON("Intro Name", 2, "start", None), + getMarkerJSON("Cue Name", 3.14, "mid", None), + getMarkerJSON("Outro Name", 4, "end", None), + getMarkerJSON("Start Loop", 2, "start", "The Best Loop 1"), + getMarkerJSON("Mid Loop", 3, "mid", "The Best Loop 1"), + getMarkerJSON("End Loop", 3.5, "end", "The Best Loop 1"), + ] + # Command, Weight?/itemid? (-1 is loaded), marker json (Intro at 2 seconds.) + self._send_msg_wait_OKAY("SETMARKER:0," + markers[0]) + self._send_msg_wait_OKAY("SETMARKER:0," + markers[1]) + self._send_msg_wait_OKAY("SETMARKER:1," + markers[2]) + self._send_msg_wait_OKAY("SETMARKER:-1," + markers[3]) + self._send_msg_wait_OKAY("SETMARKER:-1," + markers[4]) + self._send_msg_wait_OKAY("SETMARKER:-1," + markers[5]) + + # Test we didn't completely break the player + response = self._send_msg_wait_OKAY("STATUS") + self.assertTrue(response) + json_obj = json.loads(response) + + # Now test that all the markers we setup are present. + item0 = json_obj["show_plan"][0] + self.assertEquals(item0["weight"], 0) + self.assertEquals(item0["intro"], 2) # Backwards compat with basic Webstudio intro/cue/outro + self.assertEquals(item0["cue"], 3.14) + self.assertEquals(item0["markers"], markers[0:2]) # Check the full marker configs match + + item = json_obj["show_plan"][1] + self.assertEquals(item["weight"], 0) + self.assertEquals(item["outro"], 4) + self.assertEquals(item["markers"], [markers[2]]) + + # In this case, we want to make sure both the current and loaded items are updated + for item in [json_obj["show_plan"][2], json_obj["loaded_item"]]: + self.assertEquals(item["intro"], None) # This is a loop. It should not appear as a standard intro, outro or cue. + self.assertEquals(item["outro"], None) + self.assertEquals(item["cue"], None) + self.assertEquals(item["markers"], markers[3:]) + # runs the unit tests in the module if __name__ == "__main__": From cc861e2869dce13a6515eb2719f5df5fba911296 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 10 Apr 2021 22:56:53 +0100 Subject: [PATCH 2/3] End of Day WIP on marker support --- player.py | 51 +++++++++++++++++++++++++++++++++++++--- tests/test_player.py | 2 ++ types/marker.py | 41 ++++++++++++++++++++++++++++++++ plan.py => types/plan.py | 14 +++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 types/marker.py rename plan.py => types/plan.py (87%) diff --git a/player.py b/player.py index e24b80b..6082f25 100644 --- a/player.py +++ b/player.py @@ -20,7 +20,9 @@ # that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting. # Stop the Pygame Hello message. +from logging import exception import os +from types.marker import Marker os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" from queue import Empty @@ -36,7 +38,7 @@ from mutagen.mp3 import MP3 from helpers.myradio_api import MyRadioAPI from helpers.state_manager import StateManager from helpers.logging_manager import LoggingManager -from plan import PlanItem +from types.plan import PlanItem # TODO ENUM @@ -74,6 +76,7 @@ class Player: "play_on_load": False, "output": None, "show_plan": [], + "markers": [], } __rate_limited_params = ["pos", "pos_offset", "pos_true", "remaining"] @@ -402,7 +405,48 @@ class Player: return True - def ended(self): + def set_marker(self, timeslotitemid: int, marker_str: str): + set_loaded = False + success = True + try: + marker = Marker(marker_str) + except Exception as e: + self.logger.log.error("Failed to create Marker instance with {} {}: {}".format(timeslotitemid, marker_str, e)) + return False + + if timeslotitemid == -1: + set_loaded = True + if not self.isLoaded: + return False + timeslotitemid = self.state.state["loaded_item"]["timeslotitemid"] + + for i in range(len(self.state.state["show_plan"])): + plan = self.state.state["show_plan"] + item = plan[i] + + if item.timeslotitemid == timeslotitemid: + try: + item = item.set_marker(marker) + + self.state.update("show_plan", plan) + except Exception as e: + self.logger.log.error( + "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.state["show_plan"]["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)) + success = False + + return success + + # Helper functions + + def _ended(self): loaded_item = self.state.state["loaded_item"] # Track has ended @@ -459,7 +503,7 @@ class Player: and not self.isPlaying and not self.stopped_manually ): - self.ended() + self._ended() self.state.update("playing", self.isPlaying) self.state.update("loaded", self.isLoaded) @@ -656,6 +700,7 @@ class Player: 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])), } message_type: str = self.last_msg.split(":")[0] diff --git a/tests/test_player.py b/tests/test_player.py index c5ac8ae..9283826 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -349,6 +349,8 @@ class TestPlayer(unittest.TestCase): self.assertEquals(item["cue"], None) self.assertEquals(item["markers"], markers[3:]) + # TODO: Now test editing/deleting them + # runs the unit tests in the module if __name__ == "__main__": diff --git a/types/marker.py b/types/marker.py new file mode 100644 index 0000000..7c166ff --- /dev/null +++ b/types/marker.py @@ -0,0 +1,41 @@ +import json +from typing import Dict, Optional, Union + +POSITIONS = ["start", "mid", "end"] +PARAMS = ["name", "time", "position", "section"] + + +class Marker: + marker: Dict + + def __init__(self, marker_str: str): + try: + marker = json.loads(marker_str) + except Exception as e: + raise ValueError("Failed to decode JSON for marker: {}".format(e)) + + for key in marker.keys(): + if key not in PARAMS: + raise ValueError("Key {} is not a valid marker parameter.".format(key)) + + if not isinstance(marker["name"], str): + raise ValueError("Name is not str.") + self.name = marker["name"] + + if not isinstance(marker["time"], Union[int, float]): + raise ValueError("Time is not a float or int") + + if marker["position"] not in POSITIONS: + raise ValueError("Position is not in allowed values.") + + if not isinstance(marker["section"], Optional[str]): + raise ValueError("Section name is not str or None.") + + # If everything checks out, let's save it. + self.marker = marker + + def __str__(self): + return json.dumps(self.marker) + + def __dict__(self): + return self.marker diff --git a/plan.py b/types/plan.py similarity index 87% rename from plan.py rename to types/plan.py index 0e9f2fc..4cc4e9b 100644 --- a/plan.py +++ b/types/plan.py @@ -12,6 +12,7 @@ November 2020 """ +from types.marker import Marker from typing import Any, Dict, Optional import os @@ -90,6 +91,9 @@ class PlanItem: "name": self.name, "filename": self.filename, "length": self.length, + "intro": self.intro, + "cue": self.cue, + "outro": self.outro, } def __init__(self, new_item: Dict[str, Any]): @@ -108,9 +112,19 @@ class PlanItem: self._artist = new_item["artist"] if "artist" in new_item else None self._length = new_item["length"] + # Edit this to handle markers when MyRadio supports them + self._ + # Fix any OS specific / or \'s if self.filename: if os.path.sep == "/": self._filename = self.filename.replace("\\", "/") else: self._filename = self.filename.replace("/", "\\") + + def set_marker(self, marker: Marker): + if not isinstance(marker, Marker): + raise ValueError("Marker provided is not of type Marker.") + + # Return updated item for easy chaining. + return self From c57775525b86252306e5d5b7512b3b12885ffcdf Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Mon, 12 Apr 2021 22:59:51 +0100 Subject: [PATCH 3/3] Finish audio markers support. --- baps_types/__init__.py | 0 baps_types/marker.py | 68 +++++++++++++++++++++++++ {types => baps_types}/plan.py | 93 ++++++++++++++++++++++++++++++++--- helpers/myradio_api.py | 6 +-- helpers/state_manager.py | 5 +- player.py | 79 ++++++++++++++++++++--------- tests/test_player.py | 58 ++++++++++++---------- types/marker.py | 41 --------------- 8 files changed, 247 insertions(+), 103 deletions(-) create mode 100644 baps_types/__init__.py create mode 100644 baps_types/marker.py rename {types => baps_types}/plan.py (50%) delete mode 100644 types/marker.py diff --git a/baps_types/__init__.py b/baps_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/baps_types/marker.py b/baps_types/marker.py new file mode 100644 index 0000000..d4f6bff --- /dev/null +++ b/baps_types/marker.py @@ -0,0 +1,68 @@ +import json +from typing import Dict, Literal, Optional, Union + +POSITIONS = ["start", "mid", "end"] +PARAMS = ["name", "time", "position", "section"] + + +class Marker: + marker: Dict + + def __init__(self, new_marker: Union[str, dict]): + marker: dict + try: + if isinstance(new_marker, str): + marker = json.loads(new_marker) + else: + marker = new_marker + except Exception as e: + raise ValueError("Failed to decode JSON for marker: {}".format(e)) + + for key in marker.keys(): + if key not in PARAMS: + raise ValueError("Key {} is not a valid marker parameter.".format(key)) + + if not isinstance(marker["name"], str): + raise ValueError("Name is not str.") + + if not (isinstance(marker["time"], int) or isinstance(marker["time"], float)): + raise ValueError("Time is not a float or int") + + if marker["position"] not in POSITIONS: + raise ValueError("Position is not in allowed values.") + + if not (marker["section"] is None or isinstance(marker["section"], str)): + raise ValueError("Section name is not str or None.") + + marker["time"] = float(marker["time"]) + # If everything checks out, let's save it. + self.marker = marker + + @property + def __str__(self) -> str: + return json.dumps(self.marker) + + @property + def __dict__(self) -> dict: + return self.marker + + @property + def name(self) -> str: + return self.marker["name"] + + @property + def time(self) -> float: + return float(self.marker["time"]) + + @property + def position(self) -> str: + return self.marker["position"] + + @property + def section(self) -> Optional[str]: + return self.marker["section"] + + def same_type(self, o: object) -> bool: + if not isinstance(o, Marker): + return False + return o.position == self.position and o.section == self.section diff --git a/types/plan.py b/baps_types/plan.py similarity index 50% rename from types/plan.py rename to baps_types/plan.py index 4cc4e9b..5af502d 100644 --- a/types/plan.py +++ b/baps_types/plan.py @@ -12,10 +12,12 @@ November 2020 """ -from types.marker import Marker -from typing import Any, Dict, Optional + +import json +from typing import Any, Dict, List, Optional import os +from baps_types.marker import Marker class PlanItem: _timeslotitemid: int = 0 @@ -25,6 +27,7 @@ class PlanItem: _artist: Optional[str] _trackid: Optional[int] _managedid: Optional[int] + _markers: List[Marker] = [] @property def weight(self) -> int: @@ -78,6 +81,34 @@ class PlanItem: def type(self) -> Optional[str]: return "aux" if self.managedid else "central" + @property + def intro(self) -> float: + 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 + return 0 + + @property + def cue(self) -> float: + 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 + return 0 + + @property + def outro(self) -> float: + 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 + return 0 + + @property + def markers(self) -> List[dict]: + return [repr.__dict__ for repr in self._markers] + @property def __dict__(self): return { @@ -94,6 +125,7 @@ class PlanItem: "intro": self.intro, "cue": self.cue, "outro": self.outro, + "markers": self.markers } def __init__(self, new_item: Dict[str, Any]): @@ -111,9 +143,37 @@ class PlanItem: self._title = new_item["title"] 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 [] + ) + + # 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: + marker = { + "name": "Intro", + "time": new_item["intro"], + "position": "start", + "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: + marker = { + "name": "Cue", + "time": new_item["cue"], + "position": "mid", + "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: + marker = { + "name": "Outro", + "time": new_item["outro"], + "position": "end", + "section": None + } + self.set_marker(Marker(json.dumps(marker))) - # Edit this to handle markers when MyRadio supports them - self._ # Fix any OS specific / or \'s if self.filename: @@ -122,9 +182,30 @@ class PlanItem: else: self._filename = self.filename.replace("/", "\\") - def set_marker(self, marker: Marker): - if not isinstance(marker, Marker): + def __eq__(self, o: object) -> bool: + if not isinstance(o, PlanItem): + return False + + return o.__dict__ == self.__dict__ + + def set_marker(self, new_marker: Marker): + if not isinstance(new_marker, Marker): raise ValueError("Marker provided is not of type Marker.") + replaced = False + new_markers = [] + for marker in self._markers: + if marker.same_type(new_marker): + new_markers.append(new_marker) + # Replace marker + replaced = True + else: + new_markers.append(marker) + + if not replaced: + new_markers.append(new_marker) + + self._markers = new_markers + # Return updated item for easy chaining. return self diff --git a/helpers/myradio_api.py b/helpers/myradio_api.py index d2f1a0e..508501f 100644 --- a/helpers/myradio_api.py +++ b/helpers/myradio_api.py @@ -22,15 +22,15 @@ import json from logging import INFO import os -from plan import PlanItem +from baps_types.plan import PlanItem from helpers.os_environment import resolve_external_file_path from helpers.logging_manager import LoggingManager from helpers.state_manager import StateManager class MyRadioAPI: - # TODO Config type - logger = None + logger: LoggingManager + config: StateManager def __init__(self, logger: LoggingManager, config: StateManager): self.logger = logger diff --git a/helpers/state_manager.py b/helpers/state_manager.py index a4367d2..f3a6cd7 100644 --- a/helpers/state_manager.py +++ b/helpers/state_manager.py @@ -6,7 +6,7 @@ from datetime import datetime from copy import copy from typing import Any, List -from plan import PlanItem +from baps_types.plan import PlanItem from helpers.logging_manager import LoggingManager from helpers.os_environment import resolve_external_file_path @@ -99,9 +99,6 @@ class StateManager: self.__state = copy(state) def write_to_file(self, state): - if self.__state_in_file == state: - # No change to be updated. - return self.__state_in_file = state diff --git a/player.py b/player.py index 3f1e825..832727f 100644 --- a/player.py +++ b/player.py @@ -20,9 +20,7 @@ # that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting. # Stop the Pygame Hello message. -from logging import exception import os -from types.marker import Marker os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" from queue import Empty @@ -38,8 +36,8 @@ from mutagen.mp3 import MP3 from helpers.myradio_api import MyRadioAPI from helpers.state_manager import StateManager from helpers.logging_manager import LoggingManager -from types.plan import PlanItem - +from baps_types.plan import PlanItem +from baps_types.marker import Marker # TODO ENUM VALID_MESSAGE_SOURCES = ["WEBSOCKET", "UI", "CONTROLLER", "TEST", "ALL"] @@ -76,7 +74,6 @@ class Player: "play_on_load": False, "output": None, "show_plan": [], - "markers": [], } __rate_limited_params = ["pos", "pos_offset", "pos_true", "remaining"] @@ -91,7 +88,7 @@ class Player: return True @property - def isPlaying(self): + def isPlaying(self) -> bool: if self.isInit: return (not self.isPaused) and bool(mixer.music.get_busy()) return False @@ -102,11 +99,18 @@ class Player: @property def isLoaded(self): + return self._isLoaded() + + def _isLoaded(self, short_test: bool = False): if not self.state.state["loaded_item"]: return False if self.isPlaying: return True + # If we don't want to do any testing if it's really loaded, fine. + if short_test: + return True + # Because Pygame/SDL is annoying # We're not playing now, so we can quickly test run # If that works, we're loaded. @@ -130,6 +134,13 @@ class Player: mixer.music.set_volume(1) return True + @property + def isCued(self): + # Don't mess with playback, we only care about if it's supposed to be loaded. + if not self._isLoaded(short_test=True): + return False + return (self.state.state["pos_true"] == self.state.state["loaded_item"].cue and not self.isPlaying) + @property def status(self): state = copy.copy(self.state.state) @@ -162,7 +173,7 @@ class Player: def pause(self): try: - mixer.music.pause() + mixer.music.stop() except Exception: self.logger.log.exception("Failed to pause.") return False @@ -186,22 +197,31 @@ class Player: return True return False - def stop(self): - # if self.isPlaying or self.isPaused: + def stop(self, user_initiated: bool = False): try: mixer.music.stop() except Exception: self.logger.log.exception("Failed to stop playing.") return False - self.state.update("pos", 0) - self.state.update("pos_offset", 0) - self.state.update("pos_true", 0) self.state.update("paused", False) self.stopped_manually = True + if not self.state.state["loaded_item"]: + self.logger.log.warning("Tried to stop without a loaded item.") + return True + + # This lets users toggle (using the stop button) between cue point and 0. + if user_initiated and not self.isCued: + # if there's a cue point ant we're not at it, go there. + self.seek(self.state.state["loaded_item"].cue) + else: + # Otherwise, let's go to 0. + self.state.update("pos", 0) + self.state.update("pos_offset", 0) + self.state.update("pos_true", 0) + return True - # return False def seek(self, pos: float) -> bool: if self.isPlaying: @@ -212,6 +232,7 @@ class Player: return False return True else: + self.stopped_manually = True # Don't trigger _ended() on seeking. self.state.update("paused", True) self._updateState(pos=pos) return True @@ -328,7 +349,7 @@ class Player: if showplan[i].weight == weight: self.state.update("show_plan", index=i, value=loaded_item) break - # TODO: Update the show plan filenames + # TODO: Update the show plan filenames??? try: self.logger.log.info("Loading file: " + @@ -355,6 +376,9 @@ class Player: "Failed to update the length of item.") return False + if loaded_item.cue > 0: + self.seek(loaded_item.cue) + if self.state.state["play_on_load"]: self.play() @@ -418,17 +442,18 @@ class Player: set_loaded = True if not self.isLoaded: return False - timeslotitemid = self.state.state["loaded_item"]["timeslotitemid"] + timeslotitemid = self.state.state["loaded_item"].timeslotitemid + plan_copy: List[PlanItem] = copy.copy(self.state.state["show_plan"]) for i in range(len(self.state.state["show_plan"])): - plan = self.state.state["show_plan"] - item = plan[i] + + item = plan_copy[i] if item.timeslotitemid == timeslotitemid: try: - item = item.set_marker(marker) + new_item = item.set_marker(marker) + self.state.update("show_plan", new_item, index=i) - self.state.update("show_plan", plan) except Exception as e: self.logger.log.error( "Failed to set marker on item {}: {} with marker \n{}".format(timeslotitemid, e, marker)) @@ -436,7 +461,7 @@ class Player: if set_loaded: try: - self.state.update("loaded_item", self.state.state["show_plan"]["loaded_item"].set_marker(marker)) + self.state.update("loaded_item", self.state.state["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)) @@ -449,6 +474,9 @@ class Player: def _ended(self): loaded_item = self.state.state["loaded_item"] + if not loaded_item: + return + # Track has ended print("Finished", loaded_item.name, loaded_item.weight) @@ -604,6 +632,8 @@ class Player: str(loaded_item.filename)) 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"]) @@ -612,7 +642,7 @@ class Player: if loaded_state["playing"] is True: self.logger.log.info("Resuming.") - self.unpause() + self.unpause() # Use un-pause as we don't want to jump to a new position. else: self.logger.log.info("No file was previously loaded.") @@ -660,10 +690,11 @@ class Player: ] = { # TODO Check Types "STATUS": lambda: self._retMsg(self.status, True), # Audio Playout - "PLAY": lambda: self._retMsg(self.play()), + # 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()), "UNPAUSE": lambda: self._retMsg(self.unpause()), - "STOP": lambda: self._retMsg(self.stop()), + "STOP": lambda: self._retMsg(self.stop(user_initiated=True)), "SEEK": lambda: self._retMsg( self.seek(float(self.last_msg.split(":")[1])) ), @@ -700,7 +731,7 @@ class Player: 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])), + "SETMARKER": lambda: self._retMsg(self.set_marker(int(self.last_msg.split(":")[1]), self.last_msg.split(":", 2)[2])), } message_type: str = self.last_msg.split(":")[0] diff --git a/tests/test_player.py b/tests/test_player.py index 9283826..43e8bca 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -8,6 +8,7 @@ import json from player import Player from helpers.logging_manager import LoggingManager +from helpers.state_manager import StateManager from helpers.os_environment import isMacOS # How long to wait (by default) in secs for the player to respond. @@ -51,7 +52,7 @@ def getMarker(name: str, time: float, position: str, section: Optional[str] = No def getMarkerJSON(name: str, time: float, position: str, section: Optional[str] = None): - return str(json.dumps(getMarker(**locals()))) + return json.dumps(getMarker(**locals())) class TestPlayer(unittest.TestCase): @@ -60,12 +61,14 @@ class TestPlayer(unittest.TestCase): player_from_q: multiprocessing.Queue player_to_q: multiprocessing.Queue logger: LoggingManager + server_state: StateManager # initialization logic for the test suite declared in the test module # code that is executed before all tests in one test run @classmethod def setUpClass(cls): cls.logger = LoggingManager("Test_Player") + cls.server_state = StateManager("BAPSicleServer", cls.logger) # 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 @@ -79,7 +82,7 @@ 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) + 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. @@ -139,7 +142,7 @@ class TestPlayer(unittest.TestCase): def _send_msg_wait_OKAY( self, msg: str, sources_filter=["TEST"], timeout: int = TIMEOUT_MSG_MAX_S - ): + ) -> Optional[str]: response = self._send_msg_and_wait(msg, sources_filter, timeout) self.assertTrue(response) @@ -310,44 +313,49 @@ class TestPlayer(unittest.TestCase): self._send_msg_wait_OKAY("LOAD:2") # To test currently loaded marker sets. markers = [ - getMarkerJSON("Intro Name", 2, "start", None), + # Markers are stored as float, to compare against later, these must all be floats, despite int being supported. + getMarkerJSON("Intro Name", 2.0, "start", None), getMarkerJSON("Cue Name", 3.14, "mid", None), - getMarkerJSON("Outro Name", 4, "end", None), - getMarkerJSON("Start Loop", 2, "start", "The Best Loop 1"), - getMarkerJSON("Mid Loop", 3, "mid", "The Best Loop 1"), + getMarkerJSON("Outro Name", 4.0, "end", None), + getMarkerJSON("Start Loop", 2.0, "start", "The Best Loop 1"), + getMarkerJSON("Mid Loop", 3.0, "mid", "The Best Loop 1"), getMarkerJSON("End Loop", 3.5, "end", "The Best Loop 1"), ] # Command, Weight?/itemid? (-1 is loaded), marker json (Intro at 2 seconds.) - self._send_msg_wait_OKAY("SETMARKER:0," + markers[0]) - self._send_msg_wait_OKAY("SETMARKER:0," + markers[1]) - self._send_msg_wait_OKAY("SETMARKER:1," + markers[2]) - self._send_msg_wait_OKAY("SETMARKER:-1," + markers[3]) - self._send_msg_wait_OKAY("SETMARKER:-1," + markers[4]) - self._send_msg_wait_OKAY("SETMARKER:-1," + markers[5]) + self._send_msg_wait_OKAY("SETMARKER:0:" + markers[0]) + self._send_msg_wait_OKAY("SETMARKER:0:" + markers[1]) + self._send_msg_wait_OKAY("SETMARKER:1:" + markers[2]) + self._send_msg_wait_OKAY("SETMARKER:-1:" + markers[3]) + self._send_msg_wait_OKAY("SETMARKER:-1:" + markers[4]) + self._send_msg_wait_OKAY("SETMARKER:-1:" + markers[5]) # Test we didn't completely break the player response = self._send_msg_wait_OKAY("STATUS") self.assertTrue(response) json_obj = json.loads(response) + self.logger.log.warning(json_obj) + # time.sleep(1000000) # Now test that all the markers we setup are present. - item0 = json_obj["show_plan"][0] - self.assertEquals(item0["weight"], 0) - self.assertEquals(item0["intro"], 2) # Backwards compat with basic Webstudio intro/cue/outro - self.assertEquals(item0["cue"], 3.14) - self.assertEquals(item0["markers"], markers[0:2]) # Check the full marker configs match + 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["cue"], 3.14) + 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.assertEquals(item["weight"], 0) - self.assertEquals(item["outro"], 4) - self.assertEquals(item["markers"], [markers[2]]) + self.assertEqual(item["weight"], 1) + self.assertEqual(item["outro"], 4.0) + self.assertEqual([json.dumps(item) for item in item["markers"]], [markers[2]]) # In this case, we want to make sure both the current and loaded items are updated for item in [json_obj["show_plan"][2], json_obj["loaded_item"]]: - self.assertEquals(item["intro"], None) # This is a loop. It should not appear as a standard intro, outro or cue. - self.assertEquals(item["outro"], None) - self.assertEquals(item["cue"], None) - self.assertEquals(item["markers"], markers[3:]) + self.assertEqual(item["weight"], 2) + # This is a loop marker. It should not appear as a standard intro, outro or cue. Default of 0.0 should apply to all. + 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:]) # TODO: Now test editing/deleting them diff --git a/types/marker.py b/types/marker.py deleted file mode 100644 index 7c166ff..0000000 --- a/types/marker.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -from typing import Dict, Optional, Union - -POSITIONS = ["start", "mid", "end"] -PARAMS = ["name", "time", "position", "section"] - - -class Marker: - marker: Dict - - def __init__(self, marker_str: str): - try: - marker = json.loads(marker_str) - except Exception as e: - raise ValueError("Failed to decode JSON for marker: {}".format(e)) - - for key in marker.keys(): - if key not in PARAMS: - raise ValueError("Key {} is not a valid marker parameter.".format(key)) - - if not isinstance(marker["name"], str): - raise ValueError("Name is not str.") - self.name = marker["name"] - - if not isinstance(marker["time"], Union[int, float]): - raise ValueError("Time is not a float or int") - - if marker["position"] not in POSITIONS: - raise ValueError("Position is not in allowed values.") - - if not isinstance(marker["section"], Optional[str]): - raise ValueError("Section name is not str or None.") - - # If everything checks out, let's save it. - self.marker = marker - - def __str__(self): - return json.dumps(self.marker) - - def __dict__(self): - return self.marker