Add show plan loading, send more info to Webstudio UI.

This commit is contained in:
Matthew Stratford 2021-02-14 00:29:47 +00:00
parent 23f1da515b
commit 1a59b2d5bf
4 changed files with 214 additions and 67 deletions

View file

@ -17,34 +17,77 @@
November 2020 November 2020
""" """
import requests import requests
import json
import config import config
from plan import PlanItem from plan import PlanItem
from helpers.os_environment import resolve_external_file_path from helpers.os_environment import resolve_external_file_path
from helpers.logging_manager import LoggingManager
from logging import CRITICAL, INFO, DEBUG
class MyRadioAPI(): class MyRadioAPI():
logger = None
@classmethod def __init__(self, logger: LoggingManager):
def get_filename(cls, item: PlanItem): self.logger = logger
def get_non_api_call(self, url):
url = "{}{}".format(config.MYRADIO_BASE_URL, url)
if "?" in url:
url += "&api_key={}".format(config.API_KEY)
else:
url += "?api_key={}".format(config.API_KEY)
self._log("Requesting non-API URL: " + url)
request = requests.get(url, timeout=10)
self._log("Finished request.")
if request.status_code != 200:
self._logException("Failed to get API request. Status code: " + str(request.status_code))
self._logException(str(request.content))
return None
return request
def get_apiv2_call(self, url):
url = "{}/v2{}".format(config.MYRADIO_API_URL, url)
if "?" in url:
url += "&api_key={}".format(config.API_KEY)
else:
url += "?api_key={}".format(config.API_KEY)
self._log("Requesting API V2 URL: " + url)
request = requests.get(url, timeout=10)
self._log("Finished request.")
if request.status_code != 200:
self._logException("Failed to get API request. Status code: " + str(request.status_code))
self._logException(str(request.content))
return None
return request
def get_filename(self, item: PlanItem):
format = "mp3" # TODO: Maybe we want this customisable? format = "mp3" # TODO: Maybe we want this customisable?
if item.trackId: if item.trackId:
itemType = "track" itemType = "track"
id = item.trackId id = item.trackId
url = "{}/NIPSWeb/secure_play?trackid={}&{}&api_key={}".format(config.MYRADIO_BASE_URL, id, format, config.API_KEY) url = "/NIPSWeb/secure_play?trackid={}&{}".format(id, format)
elif item.managedId: elif item.managedId:
itemType = "managed" itemType = "managed"
id = item.managedId id = item.managedId
url = "{}/NIPSWeb/managed_play?managedid={}&api_key={}".format(config.MYRADIO_BASE_URL, id, config.API_KEY) url = "/NIPSWeb/managed_play?managedid={}".format(id)
else: else:
return None return None
request = requests.get(url, timeout=10)
if request.status_code != 200: request = self.get_non_api_call(url)
# TODO: Log something here
if not request:
return None return None
filename: str = resolve_external_file_path("/music-tmp/{}-{}.{}".format(itemType, id, format)) filename: str = resolve_external_file_path("/music-tmp/{}-{}.{}".format(itemType, id, format))
@ -53,3 +96,23 @@ class MyRadioAPI():
file.write(request.content) file.write(request.content)
return filename return filename
def get_showplan(self, timeslotid: int):
url = "/timeslot/{}/showplan".format(timeslotid)
request = self.get_apiv2_call(url)
if not request:
self._logException("Failed to get show plan.")
return None
return json.loads(request.content)["payload"]
def _log(self, text:str, level: int = INFO):
self.logger.log.log(level, "MyRadio API: " + text)
def _logException(self, text:str):
self.logger.log.exception("MyRadio API: " + text)

139
player.py
View file

@ -35,7 +35,7 @@ from plan import PlanItem
# Stop the Pygame Hello message. # Stop the Pygame Hello message.
import os import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
from pygame import mixer, NOEVENT, USEREVENT, event from pygame import mixer, NOEVENT, USEREVENT, event, init
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
from helpers.myradio_api import MyRadioAPI from helpers.myradio_api import MyRadioAPI
@ -49,7 +49,9 @@ class Player():
running = False running = False
out_q = None out_q = None
last_msg = "" last_msg = ""
last_time_update = None
logger = None logger = None
api = None
__default_state = { __default_state = {
"initialised": False, "initialised": False,
@ -225,6 +227,29 @@ class Player():
return False return False
# Show Plan Related Methods # Show Plan Related Methods
def get_plan(self, message: int):
plan = self.api.get_showplan(message)
self.clear_channel_plan()
channel = self.state.state["channel"]
if len(plan) > channel:
for plan_item in plan[str(channel)]:
try:
new_item: Dict[str, any] = {
"channelWeight": int(plan_item["weight"]),
"filename": None,
"title": plan_item["title"],
"artist": plan_item["artist"] if "artist" in plan_item.keys() else None,
"timeslotItemId": int(plan_item["timeslotitemid"]) if "timeslotitemid" in plan_item.keys() and plan_item["timeslotitemid"] != None else None,
"trackId": int(plan_item["trackid"]) if "managedid" not in plan_item.keys() and plan_item["trackid"] != None else None,
"recordId": int(plan_item["trackid"]) if "trackid" in plan_item.keys() and plan_item["trackid"] != None else None, # TODO This is wrong.
"managedId": int(plan_item["managedid"]) if "managedid" in plan_item.keys() and plan_item["managedid"] != None else None,
}
self.add_to_plan(new_item)
except:
continue
return True
def add_to_plan(self, new_item: Dict[str, Any]) -> bool: def add_to_plan(self, new_item: Dict[str, Any]) -> bool:
self.state.update("show_plan", self.state.state["show_plan"] + [PlanItem(new_item)]) self.state.update("show_plan", self.state.state["show_plan"] + [PlanItem(new_item)])
@ -261,7 +286,7 @@ class Player():
return False return False
if (loaded_item.filename == "" or loaded_item.filename == None): if (loaded_item.filename == "" or loaded_item.filename == None):
loaded_item.filename = MyRadioAPI.get_filename(item = loaded_item) loaded_item.filename = self.api.get_filename(item = loaded_item)
if not loaded_item.filename: if not loaded_item.filename:
return False return False
@ -334,6 +359,39 @@ class Player():
return True return True
def ended(self):
loaded_item = self.state.state["loaded_item"]
# check the existing state (not self.isPlaying)
# Since this is called multiple times when pygame isn't playing.
if loaded_item == None or not self.isPlaying:
return
if self.out_q:
self.out_q.put("STOPPED") # Tell clients that we've stopped playing.
# Track has ended
print("Finished", 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].channelWeight == loaded_item.channelWeight:
if len(self.state.state["show_plan"]) > i+1:
self.load(self.state.state["show_plan"][i+1].channelWeight)
break
# Repeat All
elif self.state.state["repeat"] == "ALL":
self.load(self.state.state["show_plan"][0].channelWeight)
# Play on Load
if self.state.state["play_on_load"]:
self.play()
def _updateState(self, pos: Optional[float] = None): def _updateState(self, pos: Optional[float] = None):
self.state.update("initialised", self.isInit) self.state.update("initialised", self.isInit)
@ -350,32 +408,11 @@ class Player():
self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"]) self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"])
loaded_item = self.state.state["loaded_item"] def _ping_times(self):
if loaded_item == None or self.state.state["remaining"] != 0: if self.last_time_update == None or self.last_time_update + 1 < time.time():
return self.last_time_update = time.time()
self.out_q.put("POS:" + str(int(self.state.state["pos_true"])))
# Track has ended
print("Finished", 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].channelWeight == loaded_item.channelWeight:
if len(self.state.state["show_plan"]) > i+1:
self.load(self.state.state["show_plan"][i+1].channelWeight)
break
# Repeat All
elif self.state.state["repeat"] == "ALL":
self.load(self.state.state["show_plan"][0].channelWeight)
# Play on Load
if self.state.state["play_on_load"]:
self.play()
def _retMsg(self, msg: Any, okay_str: Any = False): def _retMsg(self, msg: Any, okay_str: Any = False):
@ -389,7 +426,9 @@ class Player():
response += "FAIL:" + msg response += "FAIL:" + msg
else: else:
response += "FAIL" response += "FAIL"
self.logger.log.info(("Preparing to send: {}".format(response)))
if self.out_q: if self.out_q:
self.logger.log.info(("Sending: {}".format(response)))
self.out_q.put(response) self.out_q.put(response)
def __init__(self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue): def __init__(self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue):
@ -398,11 +437,16 @@ class Player():
setproctitle.setproctitle(process_title) setproctitle.setproctitle(process_title)
multiprocessing.current_process().name = process_title multiprocessing.current_process().name = process_title
# Init pygame, only used really for the end of playback trigger.
init()
self.running = True self.running = True
self.out_q = out_q self.out_q = out_q
self.logger = LoggingManager("channel" + str(channel)) self.logger = LoggingManager("channel" + str(channel))
self.api = MyRadioAPI(self.logger)
self.state = StateManager("channel" + str(channel), self.logger, self.state = StateManager("channel" + str(channel), self.logger,
self.__default_state, self.__rate_limited_params) self.__default_state, self.__rate_limited_params)
self.state.update("channel", channel) self.state.update("channel", channel)
@ -434,6 +478,7 @@ class Player():
while self.running: while self.running:
time.sleep(0.1) time.sleep(0.1)
self._updateState() self._updateState()
self._ping_times()
try: try:
try: try:
self.last_msg = in_q.get_nowait() self.last_msg = in_q.get_nowait()
@ -453,7 +498,7 @@ class Player():
elif self.isInit: elif self.isInit:
message_types: Dict[str, Callable[..., Any]] = { # TODO Check Types message_types: Dict[str, Callable[..., Any]] = { # TODO Check Types
"STATUS": lambda: self._retMsg(self.status, True), "STATUS": lambda: self._retMsg(self.status, True),
# Audio Playout # Audio Playout
"PLAY": lambda: self._retMsg(self.play()), "PLAY": lambda: self._retMsg(self.play()),
@ -466,6 +511,8 @@ class Player():
"PLAYONLOAD": lambda: self._retMsg(self.set_play_on_load(int(self.last_msg.split(":")[1]))), "PLAYONLOAD": lambda: self._retMsg(self.set_play_on_load(int(self.last_msg.split(":")[1]))),
# Show Plan Items # Show Plan Items
"GET_PLAN": lambda: self._retMsg(self.get_plan(int(self.last_msg.split(":")[1]))),
"LOAD": lambda: self._retMsg(self.load(int(self.last_msg.split(":")[1]))), "LOAD": lambda: self._retMsg(self.load(int(self.last_msg.split(":")[1]))),
"LOADED?": lambda: self._retMsg(self.isLoaded), "LOADED?": lambda: self._retMsg(self.isLoaded),
"UNLOAD": lambda: self._retMsg(self.unload()), "UNLOAD": lambda: self._retMsg(self.unload()),
@ -479,6 +526,12 @@ class Player():
if message_type in message_types.keys(): if message_type in message_types.keys():
message_types[message_type]() message_types[message_type]()
if message_type != "STATUS":
## Then a super hacky hack. Send the status again to update Webstudio
self._updateState()
self.last_msg = "STATUS"
self._retMsg(self.status, True)
elif (self.last_msg == 'QUIT'): elif (self.last_msg == 'QUIT'):
self.running = False self.running = False
continue continue
@ -486,23 +539,25 @@ class Player():
else: else:
self._retMsg("Unknown Command") self._retMsg("Unknown Command")
else: else:
if (self.last_msg == 'STATUS'): if (self.last_msg == 'STATUS'):
self._retMsg(self.status) self._retMsg(self.status)
else: else:
self._retMsg(False) self._retMsg(False)
#try:
#callback_event = event.poll()
#print(callback_event) try:
#if callback_event.type == PLAYBACK_END: callback_event = event.poll()
# if self.out_q: if callback_event.type == PLAYBACK_END:
# print("Playback endded at end of Track.") self.ended()
# self.out_q.put("STOP") # Tell clients that we've stopped playing. else:
#elif callback_event.type == NOEVENT: pass
# pass except Exception as e:
#print("Another message") pass
#except:
# pass
# Catch the player being killed externally. # Catch the player being killed externally.
except KeyboardInterrupt: except KeyboardInterrupt:
self.logger.log.info("Received KeyboardInterupt") self.logger.log.info("Received KeyboardInterupt")
@ -510,8 +565,8 @@ class Player():
except SystemExit: except SystemExit:
self.logger.log.info("Received SystemExit") self.logger.log.info("Received SystemExit")
break break
except: except Exception as e:
self.logger.log.exception("Received unexpected exception.") self.logger.log.exception("Received unexpected exception: {}".format(e))
break break
self.logger.log.info("Quiting player ", channel) self.logger.log.info("Quiting player ", channel)

View file

@ -24,6 +24,7 @@ from typing import Any, Optional
import json import json
import setproctitle import setproctitle
import logging import logging
from helpers.os_environment import isMacOS from helpers.os_environment import isMacOS
from helpers.device_manager import DeviceManager from helpers.device_manager import DeviceManager
@ -48,6 +49,8 @@ default_state = {
"num_channels": 3 "num_channels": 3
} }
logger = None
state = None
class BAPSicleServer(): class BAPSicleServer():
@ -57,6 +60,13 @@ class BAPSicleServer():
setproctitle.setproctitle(process_title) setproctitle.setproctitle(process_title)
multiprocessing.current_process().name = process_title multiprocessing.current_process().name = process_title
global logger
global state
logger = LoggingManager("BAPSicleServer")
state = StateManager("BAPSicleServer", logger, default_state)
state.update("server_version", config.VERSION)
asyncio.get_event_loop().run_until_complete(startServer()) asyncio.get_event_loop().run_until_complete(startServer())
asyncio.get_event_loop().run_forever() asyncio.get_event_loop().run_forever()
@ -69,16 +79,21 @@ class PlayerHandler():
for channel in range(len(channel_from_q)): for channel in range(len(channel_from_q)):
try: try:
message = channel_from_q[channel].get_nowait() message = channel_from_q[channel].get_nowait()
websocket_to_q[channel].put(message) print("Player Handler saw:", message.split(":")[0])
ui_to_q[channel].put(message) try:
websocket_to_q[channel].put_nowait(message)
except Exception as e:
print(e)
pass
try:
ui_to_q[channel].put_nowait(message)
except Exception as e:
print(e)
pass
except: except:
pass pass
time.sleep(0.01) time.sleep(0.01)
logger = LoggingManager("BAPSicleServer")
state = StateManager("BAPSicleServer", logger, default_state)
state.update("server_version", config.VERSION)
app = Flask(__name__, static_url_path='') app = Flask(__name__, static_url_path='')
@ -307,6 +322,14 @@ def channel_json(channel: int):
except: except:
return status(channel) return status(channel)
@app.route("/plan/load/<int:timeslotid>")
def load_showplan(timeslotid: int):
for channel in channel_to_q:
channel.put("GET_PLAN:" + str(timeslotid))
return ui_status()
def status(channel: int): def status(channel: int):
channel_to_q[channel].put("STATUS") channel_to_q[channel].put("STATUS")
i = 0 i = 0

View file

@ -24,6 +24,7 @@ async def websocket_handler(websocket, path):
async for message in websocket: async for message in websocket:
data = json.loads(message) data = json.loads(message)
channel = int(data["channel"]) channel = int(data["channel"])
print(data)
if "command" in data.keys(): if "command" in data.keys():
if data["command"] == "PLAY": if data["command"] == "PLAY":
channel_to_q[channel].put("PLAY") channel_to_q[channel].put("PLAY")
@ -38,7 +39,6 @@ async def websocket_handler(websocket, path):
elif data["command"] == "LOAD": elif data["command"] == "LOAD":
channel_to_q[channel].put("LOAD:" + str(data["weight"])) channel_to_q[channel].put("LOAD:" + str(data["weight"]))
elif data["command"] == "ADD": elif data["command"] == "ADD":
print(data)
if "managedId" in data["newItem"].keys() and isinstance(data["newItem"]["managedId"], str): if "managedId" in data["newItem"].keys() and isinstance(data["newItem"]["managedId"], str):
if data["newItem"]["managedId"].startswith("managed"): if data["newItem"]["managedId"].startswith("managed"):
managed_id = int(data["newItem"]["managedId"].split(":")[1]) managed_id = int(data["newItem"]["managedId"].split(":")[1])
@ -76,15 +76,21 @@ async def websocket_handler(websocket, path):
for channel in range(len(webstudio_to_q)): for channel in range(len(webstudio_to_q)):
try: try:
message = webstudio_to_q[channel].get_nowait() message = webstudio_to_q[channel].get_nowait()
if not message.startswith("STATUS"): command = message.split(":")[0]
continue # Ignore non state updates for now. print("Websocket Out:", command)
try: if command == "STATUS":
message = message.split("OKAY:")[1] try:
message = json.loads(message) message = message.split("OKAY:")[1]
except: message = json.loads(message)
pass except:
continue
elif command == "POS":
message = message.split(":")[1]
else:
continue
data = json.dumps({ data = json.dumps({
"command": "STATUS", "command": command,
"data": message, "data": message,
"channel": channel "channel": channel
}) })