diff --git a/api_handler.py b/api_handler.py new file mode 100644 index 0000000..a342d91 --- /dev/null +++ b/api_handler.py @@ -0,0 +1,38 @@ +import json +from multiprocessing import Queue +from helpers.logging_manager import LoggingManager +from helpers.myradio_api import MyRadioAPI + +# The API handler is needed from the main flask thread to process API requests. +# Flask is not able to handle these during page loads, requests.get() hangs. +# TODO: This is single threadded, but it probably doesn't need to be multi. +class APIHandler(): + logger: LoggingManager + api: MyRadioAPI + server_to_q: Queue + server_from_q: Queue + + def __init__(self, server_from_q: Queue, server_to_q: Queue): + self.server_from_q = server_from_q + self.server_to_q = server_to_q + self.logger = LoggingManager("APIHandler") + self.api = MyRadioAPI(self.logger) + + self.handle() + + def handle(self): + while self.server_from_q: + # Wait for an API request to come in. + request = self.server_from_q.get() + self.logger.log.info("Recieved Request: {}".format(request)) + if request == "LIST_PLANS": + self.server_to_q.put(request + ":" + json.dumps(self.api.get_showplans())) + elif request.startswith("SEARCH_TRACK:"): + params = request[request.index(":")+1:] + + try: + params = json.loads(params) + except Exception as e: + raise e + + self.server_to_q.put("SEARCH_TRACK:" + json.dumps(self.api.get_track_search(params["title"], params["artist"]))) diff --git a/build/requirements.txt b/build/requirements.txt index a37cbd5..76d3404 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,5 +1,6 @@ pygame==2.0.0.dev24 flask +flask-cors mutagen sounddevice autopep8 diff --git a/helpers/myradio_api.py b/helpers/myradio_api.py index 7d499a0..1dd3576 100644 --- a/helpers/myradio_api.py +++ b/helpers/myradio_api.py @@ -16,6 +16,7 @@ Date: November 2020 """ +from typing import Optional import requests import json import config @@ -69,6 +70,37 @@ class MyRadioAPI(): return request + + + + # Show plans + + + def get_showplans(self): + url = "/timeslot/currentandnext" + request = self.get_apiv2_call(url) + + if not request: + self._logException("Failed to get list of show plans.") + return None + + return json.loads(request.content)["payload"] + + 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"] + + + + # Audio Library + def get_filename(self, item: PlanItem): format = "mp3" # TODO: Maybe we want this customisable? if item.trackid: @@ -97,13 +129,12 @@ class MyRadioAPI(): return filename - def get_showplan(self, timeslotid: int): - - url = "/timeslot/{}/showplan".format(timeslotid) + def get_track_search(self, title: Optional[str], artist: Optional[str], limit: int = 100): + url = "/track/search?title={}&artist={}&digitised=1&limit={}".format(title if title else "", artist if artist else "", limit) request = self.get_apiv2_call(url) if not request: - self._logException("Failed to get show plan.") + self._logException("Failed to search for track.") return None return json.loads(request.content)["payload"] diff --git a/server.py b/server.py index f79b4fc..7a9d0fd 100644 --- a/server.py +++ b/server.py @@ -12,6 +12,7 @@ Date: October, November 2020 """ +from api_handler import APIHandler import asyncio from controllers.mattchbox_usb import MattchBox import copy @@ -40,6 +41,8 @@ from helpers.state_manager import StateManager from helpers.logging_manager import LoggingManager from websocket_server import WebsocketServer +from helpers.myradio_api import MyRadioAPI + setproctitle.setproctitle("BAPSicle - Server") default_state = { @@ -51,8 +54,9 @@ default_state = { "num_channels": 3 } -logger = None -state = None +logger: LoggingManager +state: StateManager +api: MyRadioAPI class BAPSicleServer(): @@ -65,6 +69,8 @@ class BAPSicleServer(): global logger global state logger = LoggingManager("BAPSicleServer") + global api + api = MyRadioAPI(logger) state = StateManager("BAPSicleServer", logger, default_state) state.update("server_version", config.VERSION) @@ -94,8 +100,12 @@ CORS(app, supports_credentials=True) # Allow ALL CORS!!! log = logging.getLogger('werkzeug') log.disabled = True + app.logger.disabled = True +api_from_q: queue.Queue +api_to_q: queue.Queue + channel_to_q: List[queue.Queue] = [] channel_from_q: List[queue.Queue] = [] ui_to_q: List[queue.Queue] = [] @@ -330,6 +340,65 @@ def channel_json(channel: int): except: return status(channel) + + +@app.route("/plan/list") +def list_showplans(): + while (not api_from_q.empty()): + api_from_q.get() # Just waste any previous status responses. + + api_to_q.put("LIST_PLANS") + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith("LIST_PLANS:"): + response = response[response.index(":")+1:] + #try: + # response = json.loads(response) + #except Exception as e: + # raise e + return response + + except queue.Empty: + pass + + time.sleep(0.1) + +@app.route("/library/search/") +def search_library(type: str): + + if type not in ["managed", "track"]: + abort(404) + + while (not api_from_q.empty()): + api_from_q.get() # Just waste any previous status responses. + + params = json.dumps({ + "title": request.args.get('title'), + "artist": request.args.get('artist') + }) + api_to_q.put("SEARCH_TRACK:{}".format(params)) + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith("SEARCH_TRACK:"): + response = response[response.index(":")+1:] + #try: + # response = json.loads(response) + #except Exception as e: + # raise e + return response + + except queue.Empty: + pass + + time.sleep(0.1) + + + + @app.route("/plan/load/") def load_showplan(timeslotid: int): @@ -338,12 +407,17 @@ def load_showplan(timeslotid: int): return ui_status() + + + + + def status(channel: int): while (not ui_to_q[channel].empty()): ui_to_q[channel].get() # Just waste any previous status responses. channel_to_q[channel].put("STATUS") - i = 0 + while True: try: response = ui_to_q[channel].get_nowait() @@ -430,8 +504,11 @@ async def startServer(): ) channel_p[channel].start() - - + global api_from_q, api_to_q + api_to_q = multiprocessing.Queue() + api_from_q = multiprocessing.Queue() + api_handler = multiprocessing.Process(target=APIHandler, args=(api_to_q, api_from_q)) + api_handler.start() player_handler = multiprocessing.Process(target=PlayerHandler, args=(channel_from_q, websocket_to_q, ui_to_q)) player_handler.start()