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
"""
import requests
import json
import config
from plan import PlanItem
from helpers.os_environment import resolve_external_file_path
from helpers.logging_manager import LoggingManager
from logging import CRITICAL, INFO, DEBUG
class MyRadioAPI():
logger = None
@classmethod
def get_filename(cls, item: PlanItem):
def __init__(self, logger: LoggingManager):
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?
if item.trackId:
itemType = "track"
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:
itemType = "managed"
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:
return None
request = requests.get(url, timeout=10)
if request.status_code != 200:
# TODO: Log something here
request = self.get_non_api_call(url)
if not request:
return None
filename: str = resolve_external_file_path("/music-tmp/{}-{}.{}".format(itemType, id, format))
@ -53,3 +96,23 @@ class MyRadioAPI():
file.write(request.content)
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.
import os
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 helpers.myradio_api import MyRadioAPI
@ -49,7 +49,9 @@ class Player():
running = False
out_q = None
last_msg = ""
last_time_update = None
logger = None
api = None
__default_state = {
"initialised": False,
@ -225,6 +227,29 @@ class Player():
return False
# 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:
self.state.update("show_plan", self.state.state["show_plan"] + [PlanItem(new_item)])
@ -261,7 +286,7 @@ class Player():
return False
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:
return False
@ -334,6 +359,39 @@ class Player():
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):
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"])
loaded_item = self.state.state["loaded_item"]
if loaded_item == None or self.state.state["remaining"] != 0:
return
def _ping_times(self):
if self.last_time_update == None or self.last_time_update + 1 < time.time():
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):
@ -389,7 +426,9 @@ class Player():
response += "FAIL:" + msg
else:
response += "FAIL"
self.logger.log.info(("Preparing to send: {}".format(response)))
if self.out_q:
self.logger.log.info(("Sending: {}".format(response)))
self.out_q.put(response)
def __init__(self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue):
@ -398,11 +437,16 @@ class Player():
setproctitle.setproctitle(process_title)
multiprocessing.current_process().name = process_title
# Init pygame, only used really for the end of playback trigger.
init()
self.running = True
self.out_q = out_q
self.logger = LoggingManager("channel" + str(channel))
self.api = MyRadioAPI(self.logger)
self.state = StateManager("channel" + str(channel), self.logger,
self.__default_state, self.__rate_limited_params)
self.state.update("channel", channel)
@ -434,6 +478,7 @@ class Player():
while self.running:
time.sleep(0.1)
self._updateState()
self._ping_times()
try:
try:
self.last_msg = in_q.get_nowait()
@ -453,7 +498,7 @@ class Player():
elif self.isInit:
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
"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]))),
# 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]))),
"LOADED?": lambda: self._retMsg(self.isLoaded),
"UNLOAD": lambda: self._retMsg(self.unload()),
@ -479,6 +526,12 @@ class Player():
if message_type in message_types.keys():
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'):
self.running = False
continue
@ -486,23 +539,25 @@ class Player():
else:
self._retMsg("Unknown Command")
else:
if (self.last_msg == 'STATUS'):
self._retMsg(self.status)
else:
self._retMsg(False)
#try:
#callback_event = event.poll()
#print(callback_event)
#if callback_event.type == PLAYBACK_END:
# if self.out_q:
# print("Playback endded at end of Track.")
# self.out_q.put("STOP") # Tell clients that we've stopped playing.
#elif callback_event.type == NOEVENT:
# pass
#print("Another message")
#except:
# pass
try:
callback_event = event.poll()
if callback_event.type == PLAYBACK_END:
self.ended()
else:
pass
except Exception as e:
pass
# Catch the player being killed externally.
except KeyboardInterrupt:
self.logger.log.info("Received KeyboardInterupt")
@ -510,8 +565,8 @@ class Player():
except SystemExit:
self.logger.log.info("Received SystemExit")
break
except:
self.logger.log.exception("Received unexpected exception.")
except Exception as e:
self.logger.log.exception("Received unexpected exception: {}".format(e))
break
self.logger.log.info("Quiting player ", channel)

View file

@ -24,6 +24,7 @@ from typing import Any, Optional
import json
import setproctitle
import logging
from helpers.os_environment import isMacOS
from helpers.device_manager import DeviceManager
@ -48,6 +49,8 @@ default_state = {
"num_channels": 3
}
logger = None
state = None
class BAPSicleServer():
@ -57,6 +60,13 @@ class BAPSicleServer():
setproctitle.setproctitle(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_forever()
@ -69,16 +79,21 @@ class PlayerHandler():
for channel in range(len(channel_from_q)):
try:
message = channel_from_q[channel].get_nowait()
websocket_to_q[channel].put(message)
ui_to_q[channel].put(message)
print("Player Handler saw:", message.split(":")[0])
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:
pass
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='')
@ -307,6 +322,14 @@ def channel_json(channel: int):
except:
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):
channel_to_q[channel].put("STATUS")
i = 0

View file

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