182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
import base64
|
|
import logging
|
|
import time
|
|
import traceback
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import datetime
|
|
from threading import Thread
|
|
|
|
import defusedxml.minidom
|
|
from api_clients import version1 as api_client
|
|
|
|
|
|
class ListenerStat(Thread):
|
|
|
|
HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
|
|
|
|
def __init__(self, config, logger=None):
|
|
Thread.__init__(self)
|
|
self.config = config
|
|
self.api_client = api_client.AirtimeApiClient()
|
|
if logger is None:
|
|
self.logger = logging.getLogger()
|
|
else:
|
|
self.logger = logger
|
|
|
|
def get_node_text(self, nodelist):
|
|
rc = []
|
|
for node in nodelist:
|
|
if node.nodeType == node.TEXT_NODE:
|
|
rc.append(node.data)
|
|
return "".join(rc)
|
|
|
|
def get_stream_parameters(self):
|
|
# [{"user":"", "password":"", "url":"", "port":""},{},{}]
|
|
return self.api_client.get_stream_parameters()
|
|
|
|
def get_stream_server_xml(self, ip, url, is_shoutcast=False):
|
|
auth_string = "%(admin_user)s:%(admin_pass)s" % ip
|
|
encoded = base64.b64encode(auth_string.encode("utf-8"))
|
|
|
|
header = {"Authorization": "Basic %s" % encoded.decode("ascii")}
|
|
|
|
if is_shoutcast:
|
|
# user agent is required for shoutcast auth, otherwise it returns 404.
|
|
user_agent = "Mozilla/5.0 (Linux; rv:22.0) Gecko/20130405 Firefox/22.0"
|
|
header["User-Agent"] = user_agent
|
|
|
|
req = urllib.request.Request(
|
|
# assuming that the icecast stats path is /admin/stats.xml
|
|
# need to fix this
|
|
url=url,
|
|
headers=header,
|
|
)
|
|
|
|
f = urllib.request.urlopen(req, timeout=ListenerStat.HTTP_REQUEST_TIMEOUT)
|
|
document = f.read()
|
|
|
|
return document
|
|
|
|
def get_icecast_stats(self, ip):
|
|
document = None
|
|
if "airtime.pro" in ip["host"].lower():
|
|
url = "http://%(host)s:%(port)s/stats.xsl" % ip
|
|
document = self.get_stream_server_xml(ip, url)
|
|
else:
|
|
url = "http://%(host)s:%(port)s/admin/stats.xml" % ip
|
|
document = self.get_stream_server_xml(ip, url)
|
|
dom = defusedxml.minidom.parseString(document)
|
|
sources = dom.getElementsByTagName("source")
|
|
|
|
mount_stats = None
|
|
for s in sources:
|
|
# drop the leading '/' character
|
|
mount_name = s.getAttribute("mount")[1:]
|
|
if mount_name == ip["mount"]:
|
|
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
listeners = s.getElementsByTagName("listeners")
|
|
num_listeners = 0
|
|
if len(listeners):
|
|
num_listeners = self.get_node_text(listeners[0].childNodes)
|
|
|
|
mount_stats = {
|
|
"timestamp": timestamp,
|
|
"num_listeners": num_listeners,
|
|
"mount_name": mount_name,
|
|
}
|
|
|
|
return mount_stats
|
|
|
|
def get_shoutcast_stats(self, ip):
|
|
url = "http://%(host)s:%(port)s/admin.cgi?sid=1&mode=viewxml" % ip
|
|
document = self.get_stream_server_xml(ip, url, is_shoutcast=True)
|
|
dom = defusedxml.minidom.parseString(document)
|
|
current_listeners = dom.getElementsByTagName("CURRENTLISTENERS")
|
|
|
|
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
num_listeners = 0
|
|
if len(current_listeners):
|
|
num_listeners = self.get_node_text(current_listeners[0].childNodes)
|
|
|
|
mount_stats = {
|
|
"timestamp": timestamp,
|
|
"num_listeners": num_listeners,
|
|
"mount_name": "shoutcast",
|
|
}
|
|
|
|
return mount_stats
|
|
|
|
def get_stream_stats(self, stream_parameters):
|
|
stats = []
|
|
|
|
# iterate over stream_parameters which is a list of dicts. Each dict
|
|
# represents one Airtime stream (currently this limit is 3).
|
|
# Note that there can be optimizations done, since if all three
|
|
# streams are the same server, we will still initiate 3 separate
|
|
# connections
|
|
for k, v in stream_parameters.items():
|
|
if v["enable"] == "true":
|
|
try:
|
|
if v["output"] == "icecast":
|
|
mount_stats = self.get_icecast_stats(v)
|
|
if mount_stats:
|
|
stats.append(mount_stats)
|
|
else:
|
|
stats.append(self.get_shoutcast_stats(v))
|
|
self.update_listener_stat_error(v["mount"], "OK")
|
|
except Exception as e:
|
|
try:
|
|
self.update_listener_stat_error(v["mount"], str(e))
|
|
except Exception as e:
|
|
self.logger.error("Exception: %s", e)
|
|
|
|
return stats
|
|
|
|
def push_stream_stats(self, stats):
|
|
self.api_client.push_stream_stats(stats)
|
|
|
|
def update_listener_stat_error(self, stream_id, error):
|
|
keyname = "%s_listener_stat_error" % stream_id
|
|
data = {keyname: error}
|
|
self.api_client.update_stream_setting_table(data)
|
|
|
|
def run(self):
|
|
# Wake up every 120 seconds and gather icecast statistics. Note that we
|
|
# are currently querying the server every 2 minutes for list of
|
|
# mountpoints as well. We could remove this query if we hooked into
|
|
# rabbitmq events, and listened for these changes instead.
|
|
while True:
|
|
try:
|
|
stream_parameters = self.get_stream_parameters()
|
|
stats = self.get_stream_stats(stream_parameters["stream_params"])
|
|
|
|
if stats:
|
|
self.push_stream_stats(stats)
|
|
except Exception as e:
|
|
self.logger.error("Exception: %s", e)
|
|
|
|
time.sleep(120)
|
|
self.logger.info("ListenerStat thread exiting")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# create logger
|
|
logger = logging.getLogger("std_out")
|
|
logger.setLevel(logging.DEBUG)
|
|
# create console handler and set level to debug
|
|
# ch = logging.StreamHandler()
|
|
# ch.setLevel(logging.DEBUG)
|
|
# create formatter
|
|
formatter = logging.Formatter(
|
|
"%(asctime)s - %(name)s - %(lineno)s - %(levelname)s - %(message)s"
|
|
)
|
|
# add formatter to ch
|
|
# ch.setFormatter(formatter)
|
|
# add ch to logger
|
|
# logger.addHandler(ch)
|
|
|
|
# ls = ListenerStat(logger=logger)
|
|
# ls.run()
|