2020-11-05 23:53:27 +00:00
|
|
|
"""
|
|
|
|
BAPSicle Server
|
|
|
|
Next-gen audio playout server for University Radio York playout,
|
|
|
|
based on WebStudio interface.
|
|
|
|
|
|
|
|
MyRadio API Handler
|
|
|
|
|
2020-11-16 22:49:33 +00:00
|
|
|
In an ideal world, this module gives out and is fed PlanItems.
|
|
|
|
This means it can be swapped for a different backend in the (unlikely) event
|
|
|
|
someone else wants to integrate BAPsicle with something else.
|
|
|
|
|
2020-11-05 23:53:27 +00:00
|
|
|
Authors:
|
|
|
|
Matthew Stratford
|
|
|
|
Michael Grace
|
|
|
|
|
|
|
|
Date:
|
|
|
|
November 2020
|
|
|
|
"""
|
2021-03-21 13:05:33 +00:00
|
|
|
from typing import Optional
|
2021-04-18 02:14:14 +00:00
|
|
|
import aiohttp
|
2021-02-14 00:29:47 +00:00
|
|
|
import json
|
2021-04-22 22:00:31 +00:00
|
|
|
from logging import INFO, ERROR, WARNING
|
2021-04-08 21:21:28 +00:00
|
|
|
import os
|
2021-04-22 22:00:31 +00:00
|
|
|
import requests
|
2021-04-08 21:21:28 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
from baps_types.plan import PlanItem
|
2020-11-05 23:53:27 +00:00
|
|
|
from helpers.os_environment import resolve_external_file_path
|
2021-02-14 00:29:47 +00:00
|
|
|
from helpers.logging_manager import LoggingManager
|
2021-04-10 22:59:49 +00:00
|
|
|
from helpers.state_manager import StateManager
|
2020-11-05 23:53:27 +00:00
|
|
|
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
class MyRadioAPI:
|
2021-04-12 21:59:51 +00:00
|
|
|
logger: LoggingManager
|
|
|
|
config: StateManager
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-10 22:59:49 +00:00
|
|
|
def __init__(self, logger: LoggingManager, config: StateManager):
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger = logger
|
2021-04-10 22:59:49 +00:00
|
|
|
self.config = config
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
async def async_call(self, url, method="GET", data=None, timeout=10):
|
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async with aiohttp.ClientSession(read_timeout=timeout) as session:
|
2021-04-22 22:00:31 +00:00
|
|
|
func = session.get(url)
|
|
|
|
status_code = -1
|
|
|
|
if method == "GET":
|
|
|
|
#func = session.get(url)
|
|
|
|
status_code = 200
|
|
|
|
elif method == "POST":
|
|
|
|
func = session.post(url, data=data)
|
|
|
|
status_code = 201
|
|
|
|
elif method == "PUT":
|
|
|
|
func = session.put(url)
|
|
|
|
status_code = 201
|
|
|
|
|
|
|
|
async with func as response:
|
|
|
|
if response.status != status_code:
|
2021-04-18 02:14:14 +00:00
|
|
|
self._logException(
|
|
|
|
"Failed to get API request. Status code: " + str(response.status)
|
|
|
|
)
|
|
|
|
self._logException(str(response.text()))
|
|
|
|
return await response.read()
|
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
def call(self, url, method="GET", data=None, timeout=10, json_payload=True):
|
|
|
|
r = None
|
|
|
|
status_code = -1
|
|
|
|
if method == "GET":
|
|
|
|
r = requests.get(url, timeout=timeout)
|
|
|
|
status_code = 200
|
|
|
|
elif method == "POST":
|
|
|
|
r = requests.post(url, data, timeout=timeout)
|
|
|
|
status_code = 201
|
|
|
|
elif method == "PUT":
|
|
|
|
r = requests.put(url, data, timeout=timeout)
|
|
|
|
status_code = 200
|
|
|
|
|
|
|
|
if r.status_code != status_code:
|
|
|
|
self._logException(
|
|
|
|
"Failed to get API request. Status code: " + str(r.status_code)
|
|
|
|
)
|
|
|
|
self._logException(str(r.text))
|
|
|
|
return json.loads(r.text) if json_payload else r.text
|
|
|
|
|
|
|
|
async def async_api_call(self, url, api_version="v2", method="GET", data=None, timeout=10):
|
|
|
|
if api_version == "v2":
|
|
|
|
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
|
|
|
elif api_version == "non":
|
|
|
|
url = "{}{}".format(self.config.get()["myradio_base_url"], url)
|
|
|
|
else:
|
|
|
|
self._logException("Invalid API version. Request not sent.")
|
|
|
|
return None
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if "?" in url:
|
2021-04-18 19:27:54 +00:00
|
|
|
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
2021-04-08 19:53:51 +00:00
|
|
|
else:
|
2021-04-18 19:27:54 +00:00
|
|
|
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
self._log("Requesting API V2 URL with method {}: {}".format(method, url))
|
|
|
|
|
|
|
|
request = None
|
|
|
|
if method == "GET":
|
|
|
|
request = self.async_call(url, method="GET", timeout=timeout)
|
|
|
|
elif method == "POST":
|
|
|
|
self._log("POST data: {}".format(data))
|
|
|
|
request = self.async_call(url, data=data, method="POST", timeout=timeout)
|
|
|
|
elif method == "PUT":
|
|
|
|
request = self.async_call(url, method="PUT", timeout=timeout)
|
|
|
|
else:
|
|
|
|
self._logException("Invalid API method. Request not sent.")
|
|
|
|
return None
|
2021-04-08 19:53:51 +00:00
|
|
|
self._log("Finished request.")
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
return request
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
def api_call(self, url, api_version="v2", method="GET", data=None, timeout=10):
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
if api_version == "v2":
|
|
|
|
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
|
|
|
elif api_version == "non":
|
|
|
|
url = "{}{}".format(self.config.get()["myradio_base_url"], url)
|
|
|
|
else:
|
|
|
|
self._logException("Invalid API version. Request not sent.")
|
|
|
|
return None
|
2020-11-05 23:53:27 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if "?" in url:
|
2021-04-18 19:27:54 +00:00
|
|
|
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
2021-04-08 19:53:51 +00:00
|
|
|
else:
|
2021-04-18 19:27:54 +00:00
|
|
|
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
self._log("Requesting API V2 URL with method {}: {}".format(method, url))
|
|
|
|
|
|
|
|
request = None
|
|
|
|
if method == "GET":
|
|
|
|
request = self.call(url, method="GET", timeout=timeout)
|
|
|
|
elif method == "POST":
|
|
|
|
self._log("POST data: {}".format(data))
|
|
|
|
request = self.call(url, data=data, method="POST", timeout=timeout)
|
|
|
|
elif method == "PUT":
|
|
|
|
request = self.call(url, method="PUT", timeout=timeout)
|
|
|
|
else:
|
|
|
|
self._logException("Invalid API method. Request not sent.")
|
|
|
|
return None
|
2021-04-08 19:53:51 +00:00
|
|
|
self._log("Finished request.")
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
return request
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Show plans
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_showplans(self):
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/timeslot/currentandnextobjects?n=10"
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException("Failed to get list of show plans.")
|
|
|
|
return None
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
payload = json.loads(await request)["payload"]
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not payload["current"]:
|
|
|
|
self._logException("API did not return a current show.")
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not payload["next"]:
|
|
|
|
self._logException("API did not return a list of next shows.")
|
2021-03-21 20:15:42 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
shows = []
|
|
|
|
shows.append(payload["current"])
|
|
|
|
shows.extend(payload["next"])
|
2021-03-21 20:15:42 +00:00
|
|
|
|
2021-04-17 17:28:00 +00:00
|
|
|
timeslots = []
|
|
|
|
# Remove jukebox etc
|
|
|
|
for show in shows:
|
|
|
|
if not "timeslot_id" in show:
|
|
|
|
shows.remove(show)
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# TODO filter out jukebox
|
|
|
|
return shows
|
2021-03-21 20:15:42 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_showplan(self, timeslotid: int):
|
2021-03-21 20:15:42 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/timeslot/{}/showplan".format(timeslotid)
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException("Failed to get show plan.")
|
|
|
|
return None
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-24 23:45:20 +00:00
|
|
|
payload = json.loads(await request)["payload"]
|
|
|
|
|
|
|
|
plan = {}
|
|
|
|
|
|
|
|
# Account for MyRadio api being dumb depending on if it's cached or not.
|
|
|
|
if isinstance(payload, list):
|
|
|
|
for channel in range(len(payload)):
|
|
|
|
plan[str(channel)] = payload[channel]
|
|
|
|
return plan
|
|
|
|
elif isinstance(payload, dict):
|
|
|
|
return payload
|
|
|
|
|
|
|
|
self.logger.log.error("Show plan in unknown format.")
|
|
|
|
return None
|
|
|
|
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Audio Library
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_filename(self, item: PlanItem):
|
2021-04-08 19:53:51 +00:00
|
|
|
format = "mp3" # TODO: Maybe we want this customisable?
|
|
|
|
if item.trackid:
|
|
|
|
itemType = "track"
|
|
|
|
id = item.trackid
|
|
|
|
url = "/NIPSWeb/secure_play?trackid={}&{}".format(id, format)
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
elif item.managedid:
|
|
|
|
itemType = "managed"
|
|
|
|
id = item.managedid
|
|
|
|
url = "/NIPSWeb/managed_play?managedid={}".format(id)
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
else:
|
|
|
|
return None
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Now check if the file already exists
|
|
|
|
path: str = resolve_external_file_path("/music-tmp/")
|
2021-03-21 13:05:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not os.path.isdir(path):
|
|
|
|
self._log("Music-tmp folder is missing, attempting to create.")
|
|
|
|
try:
|
|
|
|
os.mkdir(path)
|
|
|
|
except Exception as e:
|
|
|
|
self._logException("Failed to create music-tmp folder: {}".format(e))
|
|
|
|
return None
|
2020-11-16 22:49:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
filename: str = resolve_external_file_path(
|
|
|
|
"/music-tmp/{}-{}.{}".format(itemType, id, format)
|
|
|
|
)
|
2020-11-05 23:53:27 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if os.path.isfile(filename):
|
|
|
|
return filename
|
2020-11-16 22:49:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# File doesn't exist, download it.
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url, api_version="non")
|
2020-11-05 23:53:27 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
return None
|
2021-04-07 19:04:29 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
try:
|
|
|
|
with open(filename, "wb") as file:
|
2021-04-18 02:14:14 +00:00
|
|
|
file.write(await request)
|
2021-04-08 19:53:51 +00:00
|
|
|
except Exception as e:
|
|
|
|
self._logException("Failed to write music file: {}".format(e))
|
|
|
|
return None
|
2021-04-07 19:04:29 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
return filename
|
2021-04-07 19:04:29 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Gets the list of managed music playlists.
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_playlist_music(self):
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/playlist/allitonesplaylists"
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-04-07 19:04:29 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException("Failed to retrieve music playlists.")
|
|
|
|
return None
|
2021-04-07 19:04:29 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
return json.loads(await request)["payload"]
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Gets the list of managed aux playlists (sfx, beds etc.)
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_playlist_aux(self):
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/nipswebPlaylist/allmanagedplaylists"
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2020-11-16 22:49:33 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException("Failed to retrieve music playlists.")
|
|
|
|
return None
|
2020-11-05 23:53:27 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
return json.loads(await request)["payload"]
|
2020-11-05 23:53:27 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Loads the playlist items for a certain managed aux playlist
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_playlist_aux_items(self, library_id: str):
|
2021-04-08 19:53:51 +00:00
|
|
|
# Sometimes they have "aux-<ID>", we only need the index.
|
|
|
|
if library_id.index("-") > -1:
|
2021-04-08 21:48:38 +00:00
|
|
|
library_id = library_id[library_id.index("-") + 1:]
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/nipswebPlaylist/{}/items".format(library_id)
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException(
|
|
|
|
"Failed to retrieve items for aux playlist {}.".format(library_id)
|
|
|
|
)
|
|
|
|
return None
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
return json.loads(await request)["payload"]
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
# Loads the playlist items for a certain managed playlist
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_playlist_music_items(self, library_id: str):
|
2021-04-08 19:53:51 +00:00
|
|
|
url = "/playlist/{}/tracks".format(library_id)
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException(
|
|
|
|
"Failed to retrieve items for music playlist {}.".format(library_id)
|
|
|
|
)
|
|
|
|
return None
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
return json.loads(await request)["payload"]
|
2021-04-08 19:53:51 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
async def get_track_search(
|
2021-04-08 19:53:51 +00:00
|
|
|
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
|
|
|
|
)
|
2021-04-22 22:00:31 +00:00
|
|
|
request = await self.async_api_call(url)
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if not request:
|
|
|
|
self._logException("Failed to search for track.")
|
|
|
|
return None
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-18 02:14:14 +00:00
|
|
|
return json.loads(await request)["payload"]
|
2021-04-04 14:26:39 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
def post_tracklist_start(self, item: PlanItem):
|
|
|
|
if item.type != "central":
|
|
|
|
self._log("Not tracklisting, {} is not a track.".format(item.name))
|
|
|
|
return False
|
|
|
|
|
|
|
|
self._log("Tracklisting item: {}".format(item.name))
|
|
|
|
|
|
|
|
source: str = self.config.get()["myradio_api_tracklist_source"]
|
|
|
|
data = {
|
|
|
|
"trackid": item.trackid,
|
|
|
|
"sourceid": int(source) if source.isnumeric() else source
|
|
|
|
}
|
|
|
|
# Starttime and timeslotid are default in the API to current time/show.
|
|
|
|
tracklist_id = None
|
|
|
|
try:
|
|
|
|
tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)["payload"]["audiologid"]
|
|
|
|
except Exception as e:
|
|
|
|
self._logException("Failed to get tracklistid. {}".format(e))
|
|
|
|
|
|
|
|
if not tracklist_id or not isinstance(tracklist_id, int):
|
|
|
|
self._log("Failed to tracklist! API rejected tracklist.", ERROR)
|
|
|
|
return
|
|
|
|
return tracklist_id
|
|
|
|
|
|
|
|
def post_tracklist_end(self, tracklistitemid: int):
|
|
|
|
if not tracklistitemid:
|
|
|
|
self._log("Tracklistitemid is None, can't end tracklist.", WARNING)
|
|
|
|
return False
|
|
|
|
if not isinstance(tracklistitemid, int):
|
|
|
|
self._logException("Tracklistitemid '{}' is not an integer!".format(tracklistitemid))
|
|
|
|
return False
|
|
|
|
|
|
|
|
self._log("Ending tracklistitemid {}".format(tracklistitemid))
|
|
|
|
|
|
|
|
result = self.api_call("/tracklistItem/{}/endtime".format(tracklistitemid), method="PUT")
|
|
|
|
print(result)
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
def _log(self, text: str, level: int = INFO):
|
|
|
|
self.logger.log.log(level, "MyRadio API: " + text)
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
def _logException(self, text: str):
|
|
|
|
self.logger.log.exception("MyRadio API: " + text)
|