BAPSicle/helpers/myradio_api.py

406 lines
14 KiB
Python
Raw Normal View History

"""
BAPSicle Server
Next-gen audio playout server for University Radio York playout,
based on WebStudio interface.
MyRadio API Handler
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.
Authors:
Matthew Stratford
Michael Grace
Date:
November 2020
"""
2021-03-21 13:05:33 +00:00
from typing import Optional
import aiohttp
import json
2021-09-01 21:32:23 +00:00
from logging import INFO, ERROR, WARNING, DEBUG
2021-04-08 21:21:28 +00:00
import os
import requests
import time
2021-04-08 21:21:28 +00:00
2021-04-12 21:59:51 +00:00
from baps_types.plan import PlanItem
from helpers.os_environment import resolve_external_file_path
from helpers.logging_manager import LoggingManager
from helpers.state_manager import StateManager
2021-04-08 19:53:51 +00:00
class MyRadioAPI:
2021-04-12 21:59:51 +00:00
logger: LoggingManager
config: StateManager
def __init__(self, logger: LoggingManager, config: StateManager):
2021-04-08 19:53:51 +00:00
self.logger = logger
self.config = config
async def async_call(self, url, method="GET", data=None, timeout=10):
async with aiohttp.ClientSession(read_timeout=timeout) as session:
if method == "GET":
2021-09-01 23:08:39 +00:00
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
2021-09-01 23:08:39 +00:00
else:
return
async with func as response:
if response.status != status_code:
self._logException(
2021-09-11 15:49:08 +00:00
"Failed to get API request. Status code: "
+ str(response.status)
)
2021-09-08 23:35:25 +00:00
self._logException(str(await response.text()))
return None # Given the output was bad, don't forward it.
return await response.read()
def call(self, url, method="GET", data=None, timeout=10, json_payload=True):
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
2021-09-01 23:08:39 +00:00
else:
return
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
2021-09-11 15:49:08 +00:00
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-04-08 19:53:51 +00:00
if "?" in url:
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
2021-04-08 19:53:51 +00:00
else:
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
self._log("Requesting API V2 URL with method {}: {}".format(method, url))
request = None
try:
if method == "GET":
request = await self.async_call(url, method="GET", timeout=timeout)
elif method == "POST":
self._log("POST data: {}".format(data))
request = await self.async_call(url, data=data, method="POST", timeout=timeout)
elif method == "PUT":
request = await self.async_call(url, method="PUT", timeout=timeout)
else:
self._logException("Invalid API method. Request not sent.")
return None
except aiohttp.ClientError:
self._logException("Failed async API request.")
return None
2021-04-08 19:53:51 +00:00
self._log("Finished request.")
2021-04-08 19:53:51 +00:00
return request
def 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-04-08 19:53:51 +00:00
if "?" in url:
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
2021-04-08 19:53:51 +00:00
else:
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
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-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
async def get_showplans(self):
2021-04-08 19:53:51 +00:00
url = "/timeslot/currentandnextobjects?n=10"
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
payload = json.loads(request)["payload"]
2021-03-21 13:05:33 +00:00
2021-08-16 22:29:58 +00:00
shows = []
2021-04-08 19:53:51 +00:00
if not payload["current"]:
self._logException("API did not return a current show.")
2021-08-16 22:29:58 +00:00
else:
shows.append(payload["current"])
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-08-16 22:29:58 +00:00
else:
shows.extend(payload["next"])
# Remove jukebox etc
for show in shows:
2021-09-11 16:48:57 +00:00
if "timeslot_id" not in show:
shows.remove(show)
2021-04-08 19:53:51 +00:00
return shows
async def get_showplan(self, timeslotid: int):
2021-04-08 19:53:51 +00:00
url = "/timeslot/{}/showplan".format(timeslotid)
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
payload = json.loads(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-04-08 19:53:51 +00:00
# Audio Library
2021-03-21 13:05:33 +00:00
async def get_filename(self, item: PlanItem, did_download: bool = False, redownload=False):
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:
2021-04-25 23:18:50 +00:00
return (None, False) if did_download else 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
dl_suffix = ".downloading"
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))
2021-04-25 23:18:50 +00:00
return (None, False) if did_download else None
2021-04-08 19:53:51 +00:00
filename: str = resolve_external_file_path(
"/music-tmp/{}-{}.{}".format(itemType, id, format)
)
if not redownload:
# Check if we already downloaded the file. If we did, give that, unless we're forcing a redownload.
if os.path.isfile(filename):
self._log("Already got file: " + filename, DEBUG)
return (filename, False) if did_download else filename
# If something else (another channel, the preloader etc) is downloading the track, wait for it.
if os.path.isfile(filename + dl_suffix):
time_waiting_s = 0
self._log(
"Waiting for download to complete from another worker. " + filename,
DEBUG,
)
while time_waiting_s < 20:
# TODO: Make something better here.
# If the connectivity is super poor or we're loading reeaaaalllly long files,
# this may be annoying, but this is just in case somehow the other api download gives up.
if os.path.isfile(filename):
# Now the file is downloaded successfully
return (filename, False) if did_download else filename
time_waiting_s += 1
self._log("Still waiting", DEBUG)
time.sleep(1)
2021-04-08 19:53:51 +00:00
# File doesn't exist, download it.
try:
# Just create the file to stop other sources from trying to download too.
open(filename + dl_suffix, "a").close()
except Exception:
self.logger.log.exception("Couldn't create new temp file.")
return (None, False) if did_download else None
request = await self.async_api_call(url, api_version="non")
if not request or not isinstance(request, (bytes, bytearray)):
# Remove the .downloading temp file given we gave up trying to download.
os.remove(filename + dl_suffix)
2021-04-25 23:18:50 +00:00
return (None, False) if did_download else None
2021-04-07 19:04:29 +00:00
2021-04-08 19:53:51 +00:00
try:
with open(filename + dl_suffix, "wb") as file:
file.write(request)
os.rename(filename + dl_suffix, filename)
2021-04-08 19:53:51 +00:00
except Exception as e:
self._logException("Failed to write music file: {}".format(e))
2021-04-25 23:18:50 +00:00
return (None, False) if did_download else None
2021-04-07 19:04:29 +00:00
self._log("Successfully re/downloaded file.", DEBUG)
2021-04-25 23:18:50 +00:00
return (filename, True) if did_download else filename
2021-04-07 19:04:29 +00:00
2021-04-08 19:53:51 +00:00
# Gets the list of managed music playlists.
async def get_playlist_music(self):
2021-04-08 19:53:51 +00:00
url = "/playlist/allitonesplaylists"
request = await self.async_api_call(url)
2021-04-07 19:04:29 +00:00
if not request or not isinstance(request, bytes):
2021-04-08 19:53:51 +00:00
self._logException("Failed to retrieve music playlists.")
return []
2021-04-07 19:04:29 +00:00
return json.loads(request)["payload"]
2021-04-08 19:53:51 +00:00
# Gets the list of managed aux playlists (sfx, beds etc.)
async def get_playlist_aux(self):
2021-04-08 19:53:51 +00:00
url = "/nipswebPlaylist/allmanagedplaylists"
request = await self.async_api_call(url)
if not request or not isinstance(request, bytes):
2021-04-08 19:53:51 +00:00
self._logException("Failed to retrieve music playlists.")
return []
return json.loads(request)["payload"]
2021-04-08 19:53:51 +00:00
# Loads the playlist items for a certain managed aux playlist
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-09-11 16:48:57 +00:00
library_id = library_id[library_id.index("-") + 1:]
2021-04-08 19:53:51 +00:00
url = "/nipswebPlaylist/{}/items".format(library_id)
request = await self.async_api_call(url)
2021-04-04 14:26:39 +00:00
if not request or not isinstance(request, bytes):
2021-04-08 19:53:51 +00:00
self._logException(
"Failed to retrieve items for aux playlist {}.".format(library_id)
)
return []
2021-04-04 14:26:39 +00:00
return json.loads(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
async def get_playlist_music_items(self, library_id: str):
2021-04-08 19:53:51 +00:00
url = "/playlist/{}/tracks".format(library_id)
request = await self.async_api_call(url)
2021-04-04 14:26:39 +00:00
if not request or not isinstance(request, bytes):
2021-04-08 19:53:51 +00:00
self._logException(
"Failed to retrieve items for music playlist {}.".format(library_id)
)
return []
2021-04-04 14:26:39 +00:00
return json.loads(request)["payload"]
2021-04-08 19:53:51 +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
)
request = await self.async_api_call(url)
2021-04-04 14:26:39 +00:00
if not request or not isinstance(request, bytes):
2021-04-08 19:53:51 +00:00
self._logException("Failed to search for track.")
return []
2021-04-04 14:26:39 +00:00
return json.loads(request)["payload"]
2021-04-04 14:26:39 +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
2021-09-01 22:40:28 +00:00
self._log("Tracklisting item: '{}'".format(item.name))
source: str = self.config.get()["myradio_api_tracklist_source"]
data = {
"trackid": item.trackid,
2021-09-11 15:49:08 +00:00
"sourceid": int(source) if source.isnumeric() else source,
}
# Starttime and timeslotid are default in the API to current time/show.
tracklist_id = None
try:
2021-09-11 15:49:08 +00:00
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):
2021-09-11 15:49:08 +00:00
self._logException(
"Tracklistitemid '{}' is not an integer!".format(tracklistitemid)
)
return False
self._log("Ending tracklistitemid {}".format(tracklistitemid))
2021-09-08 22:36:59 +00:00
self.api_call("/tracklistItem/{}/endtime".format(tracklistitemid), method="PUT")
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-04-08 19:53:51 +00:00
def _logException(self, text: str):
self.logger.log.exception("MyRadio API: " + text)