From 6044cbb2716f23ecce9806c13b25dd29bfdfc4d8 Mon Sep 17 00:00:00 2001 From: michael-grace Date: Sat, 31 Oct 2020 18:00:15 +0000 Subject: [PATCH 1/6] config start --- .gitignore | 7 ++++++- config.py.example | 3 +++ server.py | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 config.py.example diff --git a/.gitignore b/.gitignore index 1bbdfbd..aa8746f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ - +.vscode/settings.json __pycache__/ @@ -10,6 +10,7 @@ state/ build/build-exe-config.json install/*.exe +install/nssm *.pyo @@ -20,3 +21,7 @@ build/build-exe-pyinstaller-command.bat build/build/BAPSicle/ build/output/ + +venv/ + +config.py \ No newline at end of file diff --git a/config.py.example b/config.py.example new file mode 100644 index 0000000..6f3739a --- /dev/null +++ b/config.py.example @@ -0,0 +1,3 @@ +# Flask Details +HOST: str = "localhost" +PORT: int = 5000 \ No newline at end of file diff --git a/server.py b/server.py index f35c1e4..06361a6 100644 --- a/server.py +++ b/server.py @@ -4,6 +4,7 @@ from flask import Flask, render_template, send_from_directory, request import json import sounddevice as sd import setproctitle +import config setproctitle.setproctitle("BAPSicle - Server") @@ -174,7 +175,7 @@ def startServer(): channel_p[channel].start() # Don't use reloader, it causes Nested Processes! - app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False) + app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False) def stopServer(): From 330cc32be23a175b4f6cae735d888e3dc7ce06cc Mon Sep 17 00:00:00 2001 From: michael-grace Date: Sun, 1 Nov 2020 02:35:14 +0000 Subject: [PATCH 2/6] start of channel plans --- plan.py | 46 ++++++++++++++++++++++++++ player.py | 85 ++++++++++++++++++++++++++++++++++++------------ server.py | 55 +++++++++++++++++++++++++++++++ state_manager.py | 8 ++++- 4 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 plan.py diff --git a/plan.py b/plan.py new file mode 100644 index 0000000..45b2525 --- /dev/null +++ b/plan.py @@ -0,0 +1,46 @@ +""" + BAPSicle Server + Next-gen audio playout server for University Radio York playout, + based on WebStudio interface. + + Show Plan Items + + Authors: + Michael Grace + + Date: + November 2020 +""" + +from typing import Dict + +class PlanObject: + _timeslotitemid: int = 0 + _filename: str = "" + _title: str = "" + _artist: str = "" + + @property + def timeslotitemid(self) -> int: + return self._timeslotitemid + + @property + def filename(self) -> str: + return self._filename + + @property + def name(self) -> str: + return "{0} - {1}".format(self._title, self._artist) if self._artist else self._title + + def __init__(self, new_item: Dict[str, any]): + self._timeslotitemid = new_item["timeslotitemid"] + self._filename = new_item["filename"] + self._title = new_item["title"] + self._artist = new_item["artist"] + + def __dict__(self) -> Dict[str, any]: + return { + "timeslotitemid": self.timeslotitemid, + "name": self.name, + "filename": self.filename + } \ No newline at end of file diff --git a/player.py b/player.py index 3f84325..6bf81b0 100644 --- a/player.py +++ b/player.py @@ -1,3 +1,18 @@ +""" + BAPSicle Server + Next-gen audio playout server for University Radio York playout, + based on WebStudio interface. + + Audio Player + + Authors: + Matthew Stratford + Michael Grace + + Date: + October, November 2020 +""" + # This is the player. Reliability is critical here, so we're catching # literally every exception possible and handling it. @@ -10,8 +25,10 @@ import setproctitle import copy import json import time +from typing import Callable, Dict, List from pygame import mixer from state_manager import StateManager +from plan import PlanObject from mutagen.mp3 import MP3 import os os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" @@ -25,7 +42,7 @@ class Player(): __default_state = { "initialised": False, - "filename": "", + "loaded_item": None, "channel": -1, "playing": False, "paused": False, @@ -36,7 +53,8 @@ class Player(): "remaining": 0, "length": 0, "loop": False, - "output": None + "output": None, + "show_plan": [] } @property @@ -60,7 +78,7 @@ class Player(): @property def isLoaded(self): - if not self.state.state["filename"]: + if not self.state.state["loaded_item"]: return False if self.isPlaying: return True @@ -87,7 +105,13 @@ class Player(): @property def status(self): - res = json.dumps(self.state.state) + state = copy.copy(self.state.state) + + # Not the biggest fan of this, but maybe I'll get a better solution for this later + state["loaded_item"] = state["loaded_item"].__dict__() if state["loaded_item"] else None + state["show_plan"] = [repr.__dict__() for repr in state["show_plan"]] + + res = json.dumps(state) return res def play(self, pos=0): @@ -126,7 +150,8 @@ class Player(): # if self.isPlaying or self.isPaused: try: mixer.music.stop() - except: + except Exception as e: + print("Couldn't Stop Player:", e) return False self.state.update("pos", 0) self.state.update("pos_offset", 0) @@ -147,11 +172,27 @@ class Player(): self._updateState(pos=pos) return True - def load(self, filename): + def add_to_plan(self, new_item: Dict[str, any]) -> bool: + self.state.update("show_plan", self.state.state["show_plan"] + [PlanObject(new_item)]) + return True + + def load(self, timeslotitemid: int): if not self.isPlaying: self.unload() - self.state.update("filename", filename) + updated: bool = False + + for i in range(len(self.state.state["show_plan"])): + if self.state.state["show_plan"][i].timeslotitemid == timeslotitemid: + self.state.update("loaded_item", self.state.state["show_plan"][i]) + updated = True + break + + if not updated: + print("Failed to find timeslotitemid:", timeslotitemid) + return False + + filename: str = self.state.state["loaded_item"].filename try: mixer.music.load(filename) @@ -175,7 +216,7 @@ class Player(): try: mixer.music.unload() self.state.update("paused", False) - self.state.update("filename", "") + self.state.update("loaded_item", None) except: return False return not self.isLoaded @@ -187,7 +228,7 @@ class Player(): def output(self, name=None): self.quit() self.state.update("output", name) - self.state.update("filename", "") + self.state.update("loaded_item", None) try: if name: mixer.init(44100, -16, 1, 1024, devicename=name) @@ -249,9 +290,9 @@ class Player(): print("Using default output device.") self.output() - if loaded_state["filename"]: - print("Loading filename: " + loaded_state["filename"]) - self.load(loaded_state["filename"]) + if loaded_state["loaded_item"]: + print("Loading filename: " + loaded_state["loaded_item"["filename"]]) + self.load(loaded_state["loaded_item"["timeslotitemid"]]) if loaded_state["pos_true"] != 0: print("Seeking to pos_true: " + str(loaded_state["pos_true"])) @@ -288,17 +329,21 @@ class Player(): self._retMsg(self.isLoaded) continue + elif (self.last_msg.startswith("ADD")): + split = self.last_msg.split(":") + self._retMsg(self.add_to_plan(json.loads(":".join(split[1:])))) + elif (self.last_msg == 'PLAY'): - self._retMsg(self.play()) + self._retMsg(self.play()) elif (self.last_msg == 'PAUSE'): - self._retMsg(self.pause()) + self._retMsg(self.pause()) elif (self.last_msg == 'UNPAUSE'): - self._retMsg(self.unpause()) + self._retMsg(self.unpause()) elif (self.last_msg == 'STOP'): - self._retMsg(self.stop()) + self._retMsg(self.stop()) elif (self.last_msg == 'QUIT'): self.running = False @@ -310,13 +355,13 @@ class Player(): elif (self.last_msg.startswith("LOAD")): split = self.last_msg.split(":") - self._retMsg(self.load(split[1])) + self._retMsg(self.load(int(split[1]))) elif (self.last_msg == 'UNLOAD'): - self._retMsg(self.unload()) + self._retMsg(self.unload()) elif (self.last_msg == 'STATUS'): - self._retMsg(self.status, True) + self._retMsg(self.status, True) else: self._retMsg("Unknown Command") @@ -365,7 +410,7 @@ if __name__ == "__main__": # Do some testing in_q.put("LOADED?") in_q.put("PLAY") - in_q.put("LOAD:\\Users\\matth\\Documents\\GitHub\\bapsicle\\dev\\test.mp3") + in_q.put("LOAD:dev/test.mp3") # I mean, this won't work now, this can get sorted :) in_q.put("LOADED?") in_q.put("PLAY") print("Entering infinite loop.") diff --git a/server.py b/server.py index 06361a6..1a3eff9 100644 --- a/server.py +++ b/server.py @@ -1,3 +1,18 @@ +""" + BAPSicle Server + Next-gen audio playout server for University Radio York playout, + based on WebStudio interface. + + Flask Server + + Authors: + Matthew Stratford + Michael Grace + + Date: + October, November 2020 +""" + import multiprocessing import player from flask import Flask, render_template, send_from_directory, request @@ -123,6 +138,10 @@ def output(channel, name): channel_to_q[channel].put("OUTPUT:" + name) return ui_status() +@app.route("/player//load/") +def load(channel:int, timeslotitemid: int): + channel_to_q[channel].put("LOAD:" + str(timeslotitemid)) + return ui_status() @app.route("/player//unload") def unload(channel): @@ -131,6 +150,32 @@ def unload(channel): return ui_status() +@app.route("/player//add", methods=["POST"]) +def add_to_plan(channel: int): + new_item: Dict[str, any] = { + "timeslotitemid": int(request.form["timeslotitemid"]), + "filename": request.form["filename"], + "title": request.form["title"], + "artist": request.form["artist"], + } + + channel_to_q[channel].put("ADD:" + json.dumps(new_item)) + + return new_item + +@app.route("/player//move//") +def move_plan(channel: int, timeslotitemid: int, position: int): + channel_to_q[channel].put("MOVE:" + json.dumps({"timeslotitemid": timeslotitemid, "position": position})) + + #TODO Return + return True + +@app.route("/player//remove/") +def remove_plan(channel: int, timeslotitemid: int): + channel_to_q[channel].put("REMOVE:" + timeslotitemid) + + #TODO Return + return True @app.route("/player//status") def status(channel): @@ -174,6 +219,16 @@ def startServer(): ) channel_p[channel].start() + # There is a plan for this, but I'm going to leave this here until i sort it + new_item: Dict[str, any] = { + "timeslotitemid": 0, + "filename": "dev/test.mp3", + "title": "Test File", + "artist": None, + } + + channel_to_q[0].put("ADD:" + json.dumps(new_item)) + # Don't use reloader, it causes Nested Processes! app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False) diff --git a/state_manager.py b/state_manager.py index e81cffd..4d965a5 100644 --- a/state_manager.py +++ b/state_manager.py @@ -1,3 +1,4 @@ +import copy import json import os from helpers.os_environment import resolve_external_file_path @@ -43,8 +44,13 @@ class StateManager: self.__state = state file = open(self.filepath, "w") + + # Not the biggest fan of this, but maybe I'll get a better solution for this later + state_to_json = copy.copy(state) + state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__() if state_to_json["loaded_item"] else None + state_to_json["show_plan"] = [repr.__dict__() for repr in state_to_json["show_plan"]] - file.write(json.dumps(state, indent=2, sort_keys=True)) + file.write(json.dumps(state_to_json, indent=2, sort_keys=True)) file.close() From bd0c27ddc39d69dc65d6009e2fca1023071d1acd Mon Sep 17 00:00:00 2001 From: michael-grace Date: Sun, 1 Nov 2020 02:37:56 +0000 Subject: [PATCH 3/6] tidy up some messages --- .gitignore | 4 +++- player.py | 32 +++++++++++++------------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index aa8746f..a528655 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ build/output/ venv/ -config.py \ No newline at end of file +config.py + +dev/test.mp3 \ No newline at end of file diff --git a/player.py b/player.py index 6bf81b0..54bea85 100644 --- a/player.py +++ b/player.py @@ -325,7 +325,19 @@ class Player(): elif self.isInit: - if (self.last_msg == 'LOADED?'): + message_types: Dict[str, Callable[any, bool]] = { # TODO Check Types + "PLAY": lambda: self._retMsg(self.play()), + "PAUSE": lambda: self._retMsg(self.pause()), + "UNPAUSE": lambda: self._retMsg(self.unpause()), + "STOP": lambda: self._retMsg(self.stop()), + "UNLOAD": lambda: self._retMsg(self.unload()), + "STATUS": lambda: self._retMsg(self.status, True) + } + + if self.last_msg in message_types.keys(): + message_types[self.last_msg]() + + elif (self.last_msg == 'LOADED?'): self._retMsg(self.isLoaded) continue @@ -333,18 +345,6 @@ class Player(): split = self.last_msg.split(":") self._retMsg(self.add_to_plan(json.loads(":".join(split[1:])))) - elif (self.last_msg == 'PLAY'): - self._retMsg(self.play()) - - elif (self.last_msg == 'PAUSE'): - self._retMsg(self.pause()) - - elif (self.last_msg == 'UNPAUSE'): - self._retMsg(self.unpause()) - - elif (self.last_msg == 'STOP'): - self._retMsg(self.stop()) - elif (self.last_msg == 'QUIT'): self.running = False continue @@ -357,12 +357,6 @@ class Player(): split = self.last_msg.split(":") self._retMsg(self.load(int(split[1]))) - elif (self.last_msg == 'UNLOAD'): - self._retMsg(self.unload()) - - elif (self.last_msg == 'STATUS'): - self._retMsg(self.status, True) - else: self._retMsg("Unknown Command") else: From 1e464b22476e1b9c3637cdd180171d6f90ea91f4 Mon Sep 17 00:00:00 2001 From: michael-grace Date: Mon, 2 Nov 2020 23:06:45 +0000 Subject: [PATCH 4/6] more show plan item stuffs --- player.py | 37 ++++++++++++++++++++----------------- server.py | 22 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/player.py b/player.py index 54bea85..a987aea 100644 --- a/player.py +++ b/player.py @@ -176,6 +176,19 @@ class Player(): self.state.update("show_plan", self.state.state["show_plan"] + [PlanObject(new_item)]) return True + def remove_from_plan(self, timeslotitemid: int) -> bool: + plan_copy = copy.copy(self.state.state["show_plan"]) + for i in range(len(plan_copy)): + if plan_copy[i].timeslotitemid == timeslotitemid: + plan_copy.remove(i) + self.state.update("show_plan", plan_copy) + return True + return False + + def clear_channel_plan(self) -> bool: + self.state.update("show_plan", []) + return True + def load(self, timeslotitemid: int): if not self.isPlaying: self.unload() @@ -330,33 +343,23 @@ class Player(): "PAUSE": lambda: self._retMsg(self.pause()), "UNPAUSE": lambda: self._retMsg(self.unpause()), "STOP": lambda: self._retMsg(self.stop()), + "SEEK": lambda: self._retMsg(self.seek(float(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()), - "STATUS": lambda: self._retMsg(self.status, True) + "STATUS": lambda: self._retMsg(self.status, True), + "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()) } if self.last_msg in message_types.keys(): message_types[self.last_msg]() - elif (self.last_msg == 'LOADED?'): - self._retMsg(self.isLoaded) - continue - - elif (self.last_msg.startswith("ADD")): - split = self.last_msg.split(":") - self._retMsg(self.add_to_plan(json.loads(":".join(split[1:])))) - elif (self.last_msg == 'QUIT'): self.running = False continue - elif (self.last_msg.startswith("SEEK")): - split = self.last_msg.split(":") - self._retMsg(self.seek(float(split[1]))) - - elif (self.last_msg.startswith("LOAD")): - split = self.last_msg.split(":") - self._retMsg(self.load(int(split[1]))) - else: self._retMsg("Unknown Command") else: diff --git a/server.py b/server.py index 1a3eff9..54e43ae 100644 --- a/server.py +++ b/server.py @@ -38,6 +38,7 @@ channel_to_q = [] channel_from_q = [] channel_p = [] +### General Endpoints @app.errorhandler(404) def page_not_found(e): @@ -92,6 +93,7 @@ def ui_status(): } return render_template('status.html', data=data) +### Channel Audio Options @app.route("/player//play") def play(channel): @@ -138,6 +140,8 @@ def output(channel, name): channel_to_q[channel].put("OUTPUT:" + name) return ui_status() +### Channel Items + @app.route("/player//load/") def load(channel:int, timeslotitemid: int): channel_to_q[channel].put("LOAD:" + str(timeslotitemid)) @@ -177,6 +181,15 @@ def remove_plan(channel: int, timeslotitemid: int): #TODO Return return True +@app.route("/player//clear") +def clear_channel_plan(channel: int): + channel_to_q[channel].put("CLEAR") + + #TODO Return + return True + +### General Channel Endpoints + @app.route("/player//status") def status(channel): @@ -198,7 +211,14 @@ def status(channel): def all_stop(): for channel in channel_to_q: channel.put("STOP") - ui_status() + return ui_status() + + +@app.route("/player/all/clear") +def clear_all_channels(): + for channel in channel_to_q: + channel.put("CLEAR") + return ui_status() @app.route('/static/') From ec6d3b08549e3967073feb685c67c031cef67580 Mon Sep 17 00:00:00 2001 From: michael-grace Date: Tue, 3 Nov 2020 00:32:43 +0000 Subject: [PATCH 5/6] welcome to BAPSicle and some data types --- .gitignore | 2 +- build/requirements.txt | 3 ++- config.py.example | 5 ++++- plan.py | 18 +++++++++++------- player.py | 14 ++++++++------ server.py | 25 +++++++++++++++++++++---- state_manager.py | 9 +++++++-- 7 files changed, 54 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index a528655..f7daaed 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ venv/ config.py -dev/test.mp3 \ No newline at end of file +dev/welcome.mp3 \ No newline at end of file diff --git a/build/requirements.txt b/build/requirements.txt index 95b5aa9..6f7f474 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -3,4 +3,5 @@ flask mutagen sounddevice autopep8 -setproctitle \ No newline at end of file +setproctitle +pyttsx3 \ No newline at end of file diff --git a/config.py.example b/config.py.example index 6f3739a..b595413 100644 --- a/config.py.example +++ b/config.py.example @@ -1,3 +1,6 @@ # Flask Details HOST: str = "localhost" -PORT: int = 5000 \ No newline at end of file +PORT: int = 5000 + +# BAPSicle Details +VERSION: float = 1.0 \ No newline at end of file diff --git a/plan.py b/plan.py index 45b2525..072bc34 100644 --- a/plan.py +++ b/plan.py @@ -32,15 +32,19 @@ class PlanObject: def name(self) -> str: return "{0} - {1}".format(self._title, self._artist) if self._artist else self._title + @property + def __dict__(self) -> Dict[str, any]: + return { + "timeslotitemid": self.timeslotitemid, + "title": self._title, + "artist": self._artist, + "name": self.name, + "filename": self.filename + } + def __init__(self, new_item: Dict[str, any]): self._timeslotitemid = new_item["timeslotitemid"] self._filename = new_item["filename"] self._title = new_item["title"] self._artist = new_item["artist"] - - def __dict__(self) -> Dict[str, any]: - return { - "timeslotitemid": self.timeslotitemid, - "name": self.name, - "filename": self.filename - } \ No newline at end of file + \ No newline at end of file diff --git a/player.py b/player.py index a987aea..00134f8 100644 --- a/player.py +++ b/player.py @@ -108,8 +108,8 @@ class Player(): state = copy.copy(self.state.state) # Not the biggest fan of this, but maybe I'll get a better solution for this later - state["loaded_item"] = state["loaded_item"].__dict__() if state["loaded_item"] else None - state["show_plan"] = [repr.__dict__() for repr in state["show_plan"]] + state["loaded_item"] = state["loaded_item"].__dict__ if state["loaded_item"] else None + state["show_plan"] = [repr.__dict__ for repr in state["show_plan"]] res = json.dumps(state) return res @@ -304,8 +304,8 @@ class Player(): self.output() if loaded_state["loaded_item"]: - print("Loading filename: " + loaded_state["loaded_item"["filename"]]) - self.load(loaded_state["loaded_item"["timeslotitemid"]]) + print("Loading filename: " + loaded_state["loaded_item"].filename) + self.load(loaded_state["loaded_item"].timeslotitemid) if loaded_state["pos_true"] != 0: print("Seeking to pos_true: " + str(loaded_state["pos_true"])) @@ -353,8 +353,10 @@ class Player(): "CLEAR": lambda: self._retMsg(self.clear_channel_plan()) } - if self.last_msg in message_types.keys(): - message_types[self.last_msg]() + 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.running = False diff --git a/server.py b/server.py index 54e43ae..9181e66 100644 --- a/server.py +++ b/server.py @@ -20,6 +20,7 @@ import json import sounddevice as sd import setproctitle import config +import pyttsx3 setproctitle.setproctitle("BAPSicle - Server") @@ -239,15 +240,31 @@ def startServer(): ) channel_p[channel].start() - # There is a plan for this, but I'm going to leave this here until i sort it + # 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. + This server is accepting connections on port {0} + The version of the server service is {1} + Please refer to the documentation included with this application for further assistance.""".format( + config.PORT, + config.VERSION + ), + "dev/welcome.mp3" + ) + text_to_speach.runAndWait() + new_item: Dict[str, any] = { "timeslotitemid": 0, - "filename": "dev/test.mp3", - "title": "Test File", - "artist": None, + "filename": "dev/welcome.mp3", + "title": "Welcome to BAPSicle", + "artist": "University Radio York", } channel_to_q[0].put("ADD:" + json.dumps(new_item)) + channel_to_q[0].put("LOAD:0") + channel_to_q[0].put("PLAY") # Don't use reloader, it causes Nested Processes! app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False) diff --git a/state_manager.py b/state_manager.py index 4d965a5..4992501 100644 --- a/state_manager.py +++ b/state_manager.py @@ -2,6 +2,7 @@ import copy import json import os from helpers.os_environment import resolve_external_file_path +from plan import PlanObject class StateManager: @@ -35,6 +36,10 @@ class StateManager: else: self.__state = json.loads(file_state) + # Turn from JSON -> PlanObject + self.__state["loaded_item"] = PlanObject(self.__state["loaded_item"]) if self.__state["loaded_item"] else None + self.__state["show_plan"] = [PlanObject(obj) for obj in self.__state["show_plan"]] + @property def state(self): return self.__state @@ -47,8 +52,8 @@ class StateManager: # Not the biggest fan of this, but maybe I'll get a better solution for this later state_to_json = copy.copy(state) - state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__() if state_to_json["loaded_item"] else None - state_to_json["show_plan"] = [repr.__dict__() for repr in state_to_json["show_plan"]] + state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__ if state_to_json["loaded_item"] else None + state_to_json["show_plan"] = [repr.__dict__ for repr in state_to_json["show_plan"]] file.write(json.dumps(state_to_json, indent=2, sort_keys=True)) From f96dbf5f36b0aa7d2f97b75216a818d6b3ccc5e9 Mon Sep 17 00:00:00 2001 From: michael-grace Date: Tue, 3 Nov 2020 01:07:25 +0000 Subject: [PATCH 6/6] auto advance, repeats, play on load (not tested) --- player.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------- server.py | 15 +++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/player.py b/player.py index 00134f8..7b4bd89 100644 --- a/player.py +++ b/player.py @@ -52,7 +52,9 @@ class Player(): "pos_true": 0, "remaining": 0, "length": 0, - "loop": False, + "auto_advance": True, + "repeat": "NONE", #NONE, ONE or ALL + "play_on_load": False, "output": None, "show_plan": [] } @@ -114,6 +116,8 @@ class Player(): res = json.dumps(state) return res + ### Audio Playout Related Methods + def play(self, pos=0): # if not self.isPlaying: try: @@ -172,6 +176,35 @@ class Player(): self._updateState(pos=pos) return True + def set_auto_advance(self, message: int) -> bool: + if message == 0: + self.state.update("auto_advance", False) + return True # It did it + elif message == 1: + self.state.update("auto_advance", True) + return True + else: + return False + + def set_repeat(self, message: str) -> bool: + if message in ["ALL", "ONE", "NONE"]: + self.state.update("repeat", message) + return True + else: + return False + + def set_play_on_load(self, message: int) -> bool: + if message == 0: + self.state.update("play_on_load", False) + return True # It did it + elif message == 1: + self.state.update("play_on_load", True) + return True + else: + return False + + ### Show Plan Related Methods + def add_to_plan(self, new_item: Dict[str, any]) -> bool: self.state.update("show_plan", self.state.state["show_plan"] + [PlanObject(new_item)]) return True @@ -270,6 +303,31 @@ class Player(): self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"]) + if self.state.state["remaining"] == 0: + # Track has ended + print("Finished", self.state.state["loaded_item"].name) + + # Repeat 1 + if self.state.state["repeat"] == "ONE": + self.play() + + # Auto Advance + elif self.state.state["auto_advance"]: + for i in range(len(self.state.state["show_plan"])): + if self.state.state["show_plan"][i].timeslotitemid == self.state.state["loaded_item"].timeslotitemid: + if len(self.state.state["show_plan"]) > i+1: + self.load(self.state.state["show_plan"][i+1].timeslotitemid) + break + + # Repeat All + elif self.state.state["repeat"] == "ALL": + self.load(self.state.state["show_plan"][0].timeslotitemid) + + # Play on Load + if self.state.state["play_on_load"]: + self.play() + + def _retMsg(self, msg, okay_str=False): response = self.last_msg + ":" if msg == True: @@ -339,18 +397,25 @@ class Player(): elif self.isInit: message_types: Dict[str, Callable[any, bool]] = { # TODO Check Types - "PLAY": lambda: self._retMsg(self.play()), - "PAUSE": lambda: self._retMsg(self.pause()), - "UNPAUSE": lambda: self._retMsg(self.unpause()), - "STOP": lambda: self._retMsg(self.stop()), - "SEEK": lambda: self._retMsg(self.seek(float(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()), - "STATUS": lambda: self._retMsg(self.status, True), - "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()) + "STATUS": lambda: self._retMsg(self.status, True), + + # Audio Playout + "PLAY": lambda: self._retMsg(self.play()), + "PAUSE": lambda: self._retMsg(self.pause()), + "UNPAUSE": lambda: self._retMsg(self.unpause()), + "STOP": lambda: self._retMsg(self.stop()), + "SEEK": lambda: self._retMsg(self.seek(float(self.last_msg.split(":")[1]))), + "AUTOADVANCE": lambda: self._retMsg(self.set_auto_advance(int(self.last_msg.split(":")[1]))), + "REPEAT": lambda: self._retMsg(self.set_repeat(self.last_msg.split(":")[1])), + "PLAYONLOAD": lambda: self._retMsg(self.set_play_on_load(int(self.last_msg.split(":")[1]))), + + # Show Plan Items + "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()) } message_type: str = self.last_msg.split(":")[0] diff --git a/server.py b/server.py index 9181e66..6139b3a 100644 --- a/server.py +++ b/server.py @@ -141,6 +141,21 @@ def output(channel, name): channel_to_q[channel].put("OUTPUT:" + name) return ui_status() +@app.route("/player//autoadvance/") +def autoadvance(channel: int, state: int): + channel_to_q[channel].put("AUTOADVANCE:" + str(state)) + return ui_status() + +@app.route("/player//repeat/") +def autoadvance(channel: int, state): + channel_to_q[channel].put("REPEAT:" + state.upper()) + return ui_status() + +@app.route("/player//playonload/") +def autoadvance(channel: int, state: int): + channel_to_q[channel].put("PLAYONLOAD:" + str(state)) + return ui_status() + ### Channel Items @app.route("/player//load/")