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-09-01 21:32:23 +00:00
from logging import INFO , ERROR , WARNING , DEBUG
2021-04-08 21:21:28 +00:00
import os
2021-04-22 22:00:31 +00:00
import requests
2021-05-02 14:41:14 +00:00
import time
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
if method == " GET " :
2021-09-01 23:08:39 +00:00
func = session . get ( url )
2021-04-22 22:00:31 +00:00
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
2021-04-22 22:00:31 +00:00
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 ) :
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
2021-04-22 22:00:31 +00:00
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-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 " ] )
2021-03-21 20:15:42 +00:00
2021-04-17 17:28:00 +00:00
# Remove jukebox etc
for show in shows :
if not " timeslot_id " in show :
shows . remove ( show )
2021-04-08 19:53:51 +00:00
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-25 23:18:50 +00:00
async def get_filename ( self , item : PlanItem , did_download : bool = 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
2021-05-02 14:41:14 +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
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 )
)
2021-05-02 14:41:14 +00:00
# Check if we already downloaded the file. If we did, give that.
2021-04-08 19:53:51 +00:00
if os . path . isfile ( filename ) :
2021-09-01 21:32:23 +00:00
self . _log ( " Already got file: " + filename , DEBUG )
2021-04-25 23:18:50 +00:00
return ( filename , False ) if did_download else filename
2020-11-16 22:49:33 +00:00
2021-05-02 14:41:14 +00:00
# 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
2021-09-01 21:32:23 +00:00
self . _log ( " Waiting for download to complete from another worker. " + filename , DEBUG )
2021-05-02 14:41:14 +00:00
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
2021-09-01 21:32:23 +00:00
self . _log ( " Still waiting " , DEBUG )
2021-05-02 14:41:14 +00:00
time . sleep ( 1 )
2021-04-08 19:53:51 +00:00
# File doesn't exist, download it.
2021-05-02 14:41:14 +00:00
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
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 :
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 :
2021-05-02 14:41:14 +00:00
with open ( filename + dl_suffix , " wb " ) as file :
2021-04-18 02:14:14 +00:00
file . write ( await request )
2021-05-02 14:41:14 +00:00
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
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.
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
2021-09-01 22:40:28 +00:00
self . _log ( " Tracklisting item: ' {} ' " . format ( item . name ) )
2021-04-22 22:00:31 +00:00
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 ) )
2021-09-08 22:36:59 +00:00
self . api_call ( " /tracklistItem/ {} /endtime " . format ( tracklistitemid ) , method = " PUT " )
2021-04-22 22:00:31 +00:00
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 )