Add show plan loading, send more info to Webstudio UI.
This commit is contained in:
parent
23f1da515b
commit
1a59b2d5bf
4 changed files with 214 additions and 67 deletions
|
@ -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
139
player.py
|
@ -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)
|
||||
|
|
35
server.py
35
server.py
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue