mirror of
https://github.com/fHDHR/fHDHR_NextPVR.git
synced 2025-12-06 09:16:58 -05:00
commit
a4c7eaac5d
@ -1,2 +1,2 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
fHDHR_VERSION = "v0.2.9-beta"
|
fHDHR_VERSION = "v0.3.0-beta"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
from flask import Flask, send_from_directory, request, abort, Response, stream_with_context
|
from flask import (Flask, send_from_directory, request,
|
||||||
|
abort, Response, stream_with_context, redirect)
|
||||||
|
|
||||||
from . import hub
|
from . import hub
|
||||||
|
|
||||||
@ -24,6 +25,32 @@ class HDHR_HTTP_Server():
|
|||||||
base_url = request.headers["host"]
|
base_url = request.headers["host"]
|
||||||
return fhdhrhub.get_origin_html(base_url)
|
return fhdhrhub.get_origin_html(base_url)
|
||||||
|
|
||||||
|
@app.route('/cluster')
|
||||||
|
def cluster_html():
|
||||||
|
method = request.args.get('method', default=None, type=str)
|
||||||
|
|
||||||
|
if method == "scan":
|
||||||
|
fhdhrhub.m_search()
|
||||||
|
|
||||||
|
elif method == 'add':
|
||||||
|
fhdhrhub.cluster_add(request.args.get("location", default=None, type=str))
|
||||||
|
elif method == 'del':
|
||||||
|
fhdhrhub.cluster_del(request.args.get("location", default=None, type=str))
|
||||||
|
|
||||||
|
elif method == 'sync':
|
||||||
|
fhdhrhub.cluster_sync(request.args.get("location", default=None, type=str))
|
||||||
|
|
||||||
|
elif method == 'leave':
|
||||||
|
fhdhrhub.cluster_leave()
|
||||||
|
elif method == 'disconnect':
|
||||||
|
fhdhrhub.cluster_disconnect()
|
||||||
|
|
||||||
|
if method:
|
||||||
|
return redirect('/cluster')
|
||||||
|
|
||||||
|
base_url = request.headers["host"]
|
||||||
|
return fhdhrhub.get_cluster_html(base_url)
|
||||||
|
|
||||||
@app.route('/style.css', methods=['GET'])
|
@app.route('/style.css', methods=['GET'])
|
||||||
def style_css():
|
def style_css():
|
||||||
return send_from_directory(fhdhrhub.config.dict["filedir"]["www_dir"], 'style.css')
|
return send_from_directory(fhdhrhub.config.dict["filedir"]["www_dir"], 'style.css')
|
||||||
@ -73,6 +100,14 @@ class HDHR_HTTP_Server():
|
|||||||
response=station_list,
|
response=station_list,
|
||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
|
|
||||||
|
@app.route('/cluster.json', methods=['GET'])
|
||||||
|
def cluster_json():
|
||||||
|
base_url = request.headers["host"]
|
||||||
|
cluster_list = fhdhrhub.get_cluster_json(base_url)
|
||||||
|
return Response(status=200,
|
||||||
|
response=cluster_list,
|
||||||
|
mimetype='application/json')
|
||||||
|
|
||||||
@app.route('/xmltv.xml', methods=['GET'])
|
@app.route('/xmltv.xml', methods=['GET'])
|
||||||
def xmltv_xml():
|
def xmltv_xml():
|
||||||
base_url = request.headers["host"]
|
base_url = request.headers["host"]
|
||||||
|
|||||||
@ -38,6 +38,9 @@ class fHDHR_Hub():
|
|||||||
def get_debug_json(self, base_url):
|
def get_debug_json(self, base_url):
|
||||||
return self.files.debug.get_debug_json(base_url)
|
return self.files.debug.get_debug_json(base_url)
|
||||||
|
|
||||||
|
def get_cluster_json(self, base_url):
|
||||||
|
return self.files.cluster.get_cluster_json(base_url)
|
||||||
|
|
||||||
def get_html_error(self, message):
|
def get_html_error(self, message):
|
||||||
return self.pages.htmlerror.get_html_error(message)
|
return self.pages.htmlerror.get_html_error(message)
|
||||||
|
|
||||||
@ -73,3 +76,24 @@ class fHDHR_Hub():
|
|||||||
|
|
||||||
def get_origin_html(self, base_url):
|
def get_origin_html(self, base_url):
|
||||||
return self.pages.origin.get_origin_html(base_url)
|
return self.pages.origin.get_origin_html(base_url)
|
||||||
|
|
||||||
|
def get_cluster_html(self, base_url):
|
||||||
|
return self.pages.cluster.get_cluster_html(base_url)
|
||||||
|
|
||||||
|
def m_search(self):
|
||||||
|
self.device.ssdp.m_search()
|
||||||
|
|
||||||
|
def cluster_add(self, location):
|
||||||
|
self.device.cluster.add(location)
|
||||||
|
|
||||||
|
def cluster_del(self, location):
|
||||||
|
self.device.cluster.remove(location)
|
||||||
|
|
||||||
|
def cluster_sync(self, location):
|
||||||
|
self.device.cluster.sync(location)
|
||||||
|
|
||||||
|
def cluster_leave(self):
|
||||||
|
self.device.cluster.leave()
|
||||||
|
|
||||||
|
def cluster_disconnect(self):
|
||||||
|
self.device.cluster.disconnect()
|
||||||
|
|||||||
@ -3,6 +3,8 @@ from .tuners import Tuners
|
|||||||
from .watch import WatchStream
|
from .watch import WatchStream
|
||||||
from .images import imageHandler
|
from .images import imageHandler
|
||||||
from .station_scan import Station_Scan
|
from .station_scan import Station_Scan
|
||||||
|
from .ssdp import SSDPServer
|
||||||
|
from .cluster import fHDHR_Cluster
|
||||||
|
|
||||||
|
|
||||||
class fHDHR_Device():
|
class fHDHR_Device():
|
||||||
@ -21,3 +23,7 @@ class fHDHR_Device():
|
|||||||
self.images = imageHandler(settings, self.epg)
|
self.images = imageHandler(settings, self.epg)
|
||||||
|
|
||||||
self.station_scan = Station_Scan(settings, self.channels)
|
self.station_scan = Station_Scan(settings, self.channels)
|
||||||
|
|
||||||
|
self.ssdp = SSDPServer(settings)
|
||||||
|
|
||||||
|
self.cluster = fHDHR_Cluster(settings, self.ssdp)
|
||||||
|
|||||||
142
fHDHR/api/hub/device/cluster.py
Normal file
142
fHDHR/api/hub/device/cluster.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
import requests
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
class fHDHR_Cluster():
|
||||||
|
|
||||||
|
def __init__(self, settings, ssdp):
|
||||||
|
self.config = settings
|
||||||
|
self.ssdp = ssdp
|
||||||
|
self.cluster_file = self.config.dict["main"]["cluster"]
|
||||||
|
self.friendlyname = self.config.dict["fhdhr"]["friendlyname"]
|
||||||
|
self.location = ('http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
|
||||||
|
str(settings.dict["fhdhr"]["port"]))
|
||||||
|
self.location_url = urllib.parse.quote(self.location)
|
||||||
|
self.cluster = self.default_cluster()
|
||||||
|
self.load_cluster()
|
||||||
|
self.startup_sync()
|
||||||
|
|
||||||
|
def get_list(self):
|
||||||
|
return_dict = {}
|
||||||
|
for location in list(self.cluster.keys()):
|
||||||
|
if location != self.location:
|
||||||
|
return_dict[location] = {
|
||||||
|
"Joined": True
|
||||||
|
}
|
||||||
|
|
||||||
|
detected_list = self.ssdp.detect_method.get()
|
||||||
|
for location in detected_list:
|
||||||
|
if location not in list(self.cluster.keys()):
|
||||||
|
return_dict[location] = {
|
||||||
|
"Joined": False
|
||||||
|
}
|
||||||
|
return_dict = OrderedDict(sorted(return_dict.items()))
|
||||||
|
return return_dict
|
||||||
|
|
||||||
|
def default_cluster(self):
|
||||||
|
defdict = {}
|
||||||
|
defdict[self.location] = {
|
||||||
|
"base_url": self.location,
|
||||||
|
"name": self.friendlyname
|
||||||
|
}
|
||||||
|
return defdict
|
||||||
|
|
||||||
|
def load_cluster(self):
|
||||||
|
if os.path.isfile(self.cluster_file):
|
||||||
|
with open(self.cluster_file, 'r') as clusterfile:
|
||||||
|
self.cluster = json.load(clusterfile)
|
||||||
|
if self.location not in list(self.cluster.keys()):
|
||||||
|
self.cluster[self.location] = self.default_cluster()[self.location]
|
||||||
|
else:
|
||||||
|
self.cluster = self.default_cluster()
|
||||||
|
|
||||||
|
def startup_sync(self):
|
||||||
|
for location in list(self.cluster.keys()):
|
||||||
|
if location != self.location:
|
||||||
|
sync_url = location + "/cluster.json"
|
||||||
|
try:
|
||||||
|
sync_open = requests.get(sync_url)
|
||||||
|
retrieved_cluster = sync_open.json()
|
||||||
|
if self.location not in list(retrieved_cluster.keys()):
|
||||||
|
return self.leave()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
|
||||||
|
def save_cluster(self):
|
||||||
|
with open(self.cluster_file, 'w') as clusterfile:
|
||||||
|
clusterfile.write(json.dumps(self.cluster, indent=4))
|
||||||
|
|
||||||
|
def leave(self):
|
||||||
|
self.cluster = self.default_cluster()
|
||||||
|
self.save_cluster()
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
for location in list(self.cluster.keys()):
|
||||||
|
if location != self.location:
|
||||||
|
sync_url = location + "/cluster?method=del&location=" + self.location
|
||||||
|
try:
|
||||||
|
requests.get(sync_url)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
self.leave()
|
||||||
|
|
||||||
|
def sync(self, location):
|
||||||
|
sync_url = location + "/cluster.json"
|
||||||
|
try:
|
||||||
|
sync_open = requests.get(sync_url)
|
||||||
|
self.cluster = sync_open.json()
|
||||||
|
self.save_cluster()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
|
||||||
|
def push_sync(self):
|
||||||
|
for location in list(self.cluster.keys()):
|
||||||
|
if location != self.location:
|
||||||
|
sync_url = location + "/cluster?method=sync&location=" + self.location_url
|
||||||
|
try:
|
||||||
|
requests.get(sync_url)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
|
||||||
|
def add(self, location):
|
||||||
|
if location not in list(self.cluster.keys()):
|
||||||
|
self.cluster[location] = {"base_url": location}
|
||||||
|
|
||||||
|
location_info_url = location + "/discover.json"
|
||||||
|
try:
|
||||||
|
location_info_req = requests.get(location_info_url)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
del self.cluster[location]
|
||||||
|
return
|
||||||
|
location_info = location_info_req.json()
|
||||||
|
self.cluster[location]["name"] = location_info["FriendlyName"]
|
||||||
|
|
||||||
|
cluster_info_url = location + "/cluster.json"
|
||||||
|
try:
|
||||||
|
cluster_info_req = requests.get(cluster_info_url)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
del self.cluster[location]
|
||||||
|
return
|
||||||
|
cluster_info = cluster_info_req.json()
|
||||||
|
for cluster_key in list(cluster_info.keys()):
|
||||||
|
if cluster_key not in list(self.cluster.keys()):
|
||||||
|
self.cluster[cluster_key] = cluster_info[cluster_key]
|
||||||
|
|
||||||
|
self.push_sync()
|
||||||
|
self.save_cluster()
|
||||||
|
|
||||||
|
def remove(self, location):
|
||||||
|
if location in list(self.cluster.keys()):
|
||||||
|
del self.cluster[location]
|
||||||
|
sync_url = location + "/cluster?method=leave"
|
||||||
|
try:
|
||||||
|
requests.get(sync_url)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
self.push_sync()
|
||||||
|
self.save_cluster()
|
||||||
207
fHDHR/api/hub/device/ssdp.py
Normal file
207
fHDHR/api/hub/device/ssdp.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# Adapted from https://github.com/MoshiBin/ssdpy and https://github.com/ZeWaren/python-upnp-ssdp-example
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import json
|
||||||
|
from multiprocessing import Process
|
||||||
|
|
||||||
|
from fHDHR import fHDHR_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class fHDHR_Detect():
|
||||||
|
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.config = settings
|
||||||
|
self.ssdp_detect_file = self.config.dict["main"]["ssdp_detect"]
|
||||||
|
self.detect_list = []
|
||||||
|
|
||||||
|
def set(self, location):
|
||||||
|
if location not in self.detect_list:
|
||||||
|
self.detect_list.append(location)
|
||||||
|
with open(self.ssdp_detect_file, 'w') as ssdpdetectfile:
|
||||||
|
ssdpdetectfile.write(json.dumps(self.detect_list, indent=4))
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
if os.path.isfile(self.ssdp_detect_file):
|
||||||
|
with open(self.ssdp_detect_file, 'r') as ssdpdetectfile:
|
||||||
|
return json.load(ssdpdetectfile)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class SSDPServer():
|
||||||
|
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.config = settings
|
||||||
|
|
||||||
|
self.detect_method = fHDHR_Detect(settings)
|
||||||
|
|
||||||
|
if settings.dict["fhdhr"]["discovery_address"]:
|
||||||
|
|
||||||
|
self.sock = None
|
||||||
|
self.proto = "ipv4"
|
||||||
|
self.port = 1900
|
||||||
|
self.iface = None
|
||||||
|
self.address = None
|
||||||
|
self.server = 'fHDHR/%s UPnP/1.0' % fHDHR_VERSION
|
||||||
|
|
||||||
|
allowed_protos = ("ipv4", "ipv6")
|
||||||
|
if self.proto not in allowed_protos:
|
||||||
|
raise ValueError("Invalid proto - expected one of {}".format(allowed_protos))
|
||||||
|
|
||||||
|
self.nt = 'urn:schemas-upnp-org:device:MediaServer:1'
|
||||||
|
self.usn = 'uuid:' + settings.dict["main"]["uuid"] + '::' + self.nt
|
||||||
|
self.location = ('http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
|
||||||
|
str(settings.dict["fhdhr"]["port"]) + '/device.xml')
|
||||||
|
self.al = self.location
|
||||||
|
self.max_age = 1800
|
||||||
|
self._iface = None
|
||||||
|
|
||||||
|
if self.proto == "ipv4":
|
||||||
|
self._af_type = socket.AF_INET
|
||||||
|
self._broadcast_ip = "239.255.255.250"
|
||||||
|
self._address = (self._broadcast_ip, self.port)
|
||||||
|
self.bind_address = "0.0.0.0"
|
||||||
|
elif self.proto == "ipv6":
|
||||||
|
self._af_type = socket.AF_INET6
|
||||||
|
self._broadcast_ip = "ff02::c"
|
||||||
|
self._address = (self._broadcast_ip, self.port, 0, 0)
|
||||||
|
self.bind_address = "::"
|
||||||
|
|
||||||
|
self.broadcast_addy = "{}:{}".format(self._broadcast_ip, self.port)
|
||||||
|
|
||||||
|
self.sock = socket.socket(self._af_type, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||||
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
# Bind to specific interface
|
||||||
|
if self.iface is not None:
|
||||||
|
self.sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_BINDTODEVICE", 25), self.iface)
|
||||||
|
|
||||||
|
# Subscribe to multicast address
|
||||||
|
if self.proto == "ipv4":
|
||||||
|
mreq = socket.inet_aton(self._broadcast_ip)
|
||||||
|
if self.address is not None:
|
||||||
|
mreq += socket.inet_aton(self.address)
|
||||||
|
else:
|
||||||
|
mreq += struct.pack(b"@I", socket.INADDR_ANY)
|
||||||
|
self.sock.setsockopt(
|
||||||
|
socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq,
|
||||||
|
)
|
||||||
|
# Allow multicasts on loopback devices (necessary for testing)
|
||||||
|
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
|
||||||
|
elif self.proto == "ipv6":
|
||||||
|
# In IPv6 we use the interface index, not the address when subscribing to the group
|
||||||
|
mreq = socket.inet_pton(socket.AF_INET6, self._broadcast_ip)
|
||||||
|
if self.iface is not None:
|
||||||
|
iface_index = socket.if_nametoindex(self.iface)
|
||||||
|
# Send outgoing packets from the same interface
|
||||||
|
self.sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, iface_index)
|
||||||
|
mreq += struct.pack(b"@I", iface_index)
|
||||||
|
else:
|
||||||
|
mreq += socket.inet_pton(socket.AF_INET6, "::")
|
||||||
|
self.sock.setsockopt(
|
||||||
|
socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq,
|
||||||
|
)
|
||||||
|
self.sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
|
||||||
|
self.sock.bind((self.bind_address, self.port))
|
||||||
|
|
||||||
|
self.notify_payload = self.create_notify_payload()
|
||||||
|
self.msearch_payload = self.create_msearch_payload()
|
||||||
|
|
||||||
|
print("SSDP server Starting")
|
||||||
|
|
||||||
|
self.ssdpserve = Process(target=self.run)
|
||||||
|
self.ssdpserve.start()
|
||||||
|
|
||||||
|
self.m_search()
|
||||||
|
|
||||||
|
def on_recv(self, data, address):
|
||||||
|
# print("Received packet from {}: {}".format(address, data))
|
||||||
|
|
||||||
|
(host, port) = address
|
||||||
|
|
||||||
|
header, payload = data.decode().split('\r\n\r\n')[:2]
|
||||||
|
|
||||||
|
lines = header.split('\r\n')
|
||||||
|
cmd = lines[0].split(' ')
|
||||||
|
lines = map(lambda x: x.replace(': ', ':', 1), lines[1:])
|
||||||
|
lines = filter(lambda x: len(x) > 0, lines)
|
||||||
|
|
||||||
|
headers = [x.split(':', 1) for x in lines]
|
||||||
|
headers = dict(map(lambda x: (x[0].lower(), x[1]), headers))
|
||||||
|
|
||||||
|
if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
|
||||||
|
# SSDP discovery
|
||||||
|
# print("Received qualifying M-SEARCH from {}".format(address))
|
||||||
|
# print("M-SEARCH data: {}".format(headers))
|
||||||
|
notify = self.notify_payload
|
||||||
|
# print("Created NOTIFY: {}".format(notify))
|
||||||
|
try:
|
||||||
|
self.sock.sendto(notify, address)
|
||||||
|
except OSError: # as e:
|
||||||
|
# Most commonly: We received a multicast from an IP not in our subnet
|
||||||
|
# print("Unable to send NOTIFY to {}: {}".format(address, e))
|
||||||
|
pass
|
||||||
|
elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
|
||||||
|
# SSDP presence
|
||||||
|
# print('NOTIFY *')
|
||||||
|
# print("NOTIFY data: {}".format(headers))
|
||||||
|
if headers["server"].startswith("fHDHR"):
|
||||||
|
if headers["location"] != self.location:
|
||||||
|
self.detect_method.set(headers["location"].split("/device.xml")[0])
|
||||||
|
# else:
|
||||||
|
# print('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))
|
||||||
|
|
||||||
|
def m_search(self):
|
||||||
|
data = self.msearch_payload
|
||||||
|
self.sock.sendto(data, self._address)
|
||||||
|
|
||||||
|
def create_notify_payload(self):
|
||||||
|
if self.max_age is not None and not isinstance(self.max_age, int):
|
||||||
|
raise ValueError("max_age must by of type: int")
|
||||||
|
data = (
|
||||||
|
"NOTIFY * HTTP/1.1\r\n"
|
||||||
|
"HOST:{}\r\n"
|
||||||
|
"NT:{}\r\n"
|
||||||
|
"NTS:ssdp:alive\r\n"
|
||||||
|
"USN:{}\r\n"
|
||||||
|
"SERVER:{}\r\n"
|
||||||
|
).format(
|
||||||
|
self._broadcast_ip,
|
||||||
|
self.nt,
|
||||||
|
self.usn,
|
||||||
|
self.server
|
||||||
|
)
|
||||||
|
if self.location is not None:
|
||||||
|
data += "LOCATION:{}\r\n".format(self.location)
|
||||||
|
if self.al is not None:
|
||||||
|
data += "AL:{}\r\n".format(self.al)
|
||||||
|
if self.max_age is not None:
|
||||||
|
data += "Cache-Control:max-age={}\r\n".format(self.max_age)
|
||||||
|
data += "\r\n"
|
||||||
|
return data.encode("utf-8")
|
||||||
|
|
||||||
|
def create_msearch_payload(self):
|
||||||
|
data = (
|
||||||
|
"M-SEARCH * HTTP/1.1\r\n"
|
||||||
|
"HOST:{}\r\n"
|
||||||
|
'MAN: "ssdp:discover"\r\n'
|
||||||
|
"ST:{}\r\n"
|
||||||
|
"MX:{}\r\n"
|
||||||
|
).format(
|
||||||
|
self.broadcast_addy,
|
||||||
|
"ssdp:all",
|
||||||
|
1
|
||||||
|
)
|
||||||
|
data += "\r\n"
|
||||||
|
return data.encode("utf-8")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data, address = self.sock.recvfrom(1024)
|
||||||
|
self.on_recv(data, address)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.sock.close()
|
||||||
|
except Exception:
|
||||||
|
self.sock.close()
|
||||||
@ -7,6 +7,7 @@ from .debug_json import Debug_JSON
|
|||||||
from .lineup_status_json import Lineup_Status_JSON
|
from .lineup_status_json import Lineup_Status_JSON
|
||||||
from .xmltv_xml import xmlTV_XML
|
from .xmltv_xml import xmlTV_XML
|
||||||
from .m3u import channels_M3U
|
from .m3u import channels_M3U
|
||||||
|
from .cluster_json import Cluster_JSON
|
||||||
|
|
||||||
|
|
||||||
class fHDHR_Files():
|
class fHDHR_Files():
|
||||||
@ -26,3 +27,4 @@ class fHDHR_Files():
|
|||||||
self.m3u = channels_M3U(settings, device)
|
self.m3u = channels_M3U(settings, device)
|
||||||
|
|
||||||
self.debug = Debug_JSON(settings, device)
|
self.debug = Debug_JSON(settings, device)
|
||||||
|
self.cluster = Cluster_JSON(settings, device)
|
||||||
|
|||||||
13
fHDHR/api/hub/files/cluster_json.py
Normal file
13
fHDHR/api/hub/files/cluster_json.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Cluster_JSON():
|
||||||
|
|
||||||
|
def __init__(self, settings, device):
|
||||||
|
self.config = settings
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
def get_cluster_json(self, base_url, force_update=False):
|
||||||
|
jsoncluster = self.device.cluster.cluster
|
||||||
|
cluster_json = json.dumps(jsoncluster, indent=4)
|
||||||
|
return cluster_json
|
||||||
@ -4,35 +4,26 @@ from io import StringIO
|
|||||||
from .htmlerror import HTMLerror
|
from .htmlerror import HTMLerror
|
||||||
from .index_html import Index_HTML
|
from .index_html import Index_HTML
|
||||||
from .origin_html import Origin_HTML
|
from .origin_html import Origin_HTML
|
||||||
|
from .cluster_html import Cluster_HTML
|
||||||
from .diagnostics_html import Diagnostics_HTML
|
from .diagnostics_html import Diagnostics_HTML
|
||||||
from .version_html import Version_HTML
|
from .version_html import Version_HTML
|
||||||
from .channel_guide_html import Channel_Guide_HTML
|
from .channel_guide_html import Channel_Guide_HTML
|
||||||
|
|
||||||
|
|
||||||
class fHDHR_Pages():
|
class fHDHR_Page_Elements():
|
||||||
|
|
||||||
def __init__(self, settings, device):
|
def __init__(self, settings, device):
|
||||||
self.config = settings
|
self.config = settings
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
self.page_elements = {
|
def get(self):
|
||||||
"top": self.pagetop(),
|
return {"top": self.pagetop(), "end": self.pageend()}
|
||||||
"end": self.pageend()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.htmlerror = HTMLerror(settings)
|
|
||||||
|
|
||||||
self.index = Index_HTML(settings, self.device, self.page_elements)
|
|
||||||
self.origin = Origin_HTML(settings, self.device, self.page_elements)
|
|
||||||
self.diagnostics = Diagnostics_HTML(settings, self.device, self.page_elements)
|
|
||||||
self.version = Version_HTML(settings, self.device, self.page_elements)
|
|
||||||
self.channel_guide = Channel_Guide_HTML(settings, self.device, self.page_elements)
|
|
||||||
|
|
||||||
def pagetop(self):
|
def pagetop(self):
|
||||||
friendlyname = self.config.dict["fhdhr"]["friendlyname"]
|
friendlyname = self.config.dict["fhdhr"]["friendlyname"]
|
||||||
servicename = str(self.config.dict["main"]["servicename"])
|
servicename = str(self.config.dict["main"]["servicename"])
|
||||||
|
|
||||||
return [
|
upper_part = [
|
||||||
"<!DOCTYPE html>",
|
"<!DOCTYPE html>",
|
||||||
"<html>",
|
"<html>",
|
||||||
|
|
||||||
@ -56,14 +47,32 @@ class fHDHR_Pages():
|
|||||||
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/guide", "Guide"),
|
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/guide", "Guide"),
|
||||||
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/version", "Version"),
|
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/version", "Version"),
|
||||||
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/diagnostics", "Diagnostics"),
|
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/diagnostics", "Diagnostics"),
|
||||||
|
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/cluster", "Cluster"),
|
||||||
|
|
||||||
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("xmltv.xml", "xmltv"),
|
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("xmltv.xml", "xmltv"),
|
||||||
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("channels.m3u", "m3u"),
|
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("channels.m3u", "m3u"),
|
||||||
|
|
||||||
"</p>",
|
"</div>",
|
||||||
"</div>"
|
|
||||||
"<hr align=\"center\" width=\"100%\">"
|
"<hr align=\"center\" width=\"100%\">"
|
||||||
]
|
]
|
||||||
|
fhdhr_list = self.device.cluster.cluster
|
||||||
|
locations = []
|
||||||
|
for location in list(fhdhr_list.keys()):
|
||||||
|
item_dict = {
|
||||||
|
"base_url": fhdhr_list[location]["base_url"],
|
||||||
|
"name": fhdhr_list[location]["name"]
|
||||||
|
}
|
||||||
|
if item_dict["name"] != friendlyname:
|
||||||
|
locations.append(item_dict)
|
||||||
|
if len(locations):
|
||||||
|
upper_part.append("<div>")
|
||||||
|
locations = sorted(locations, key=lambda i: i['name'])
|
||||||
|
for location in locations:
|
||||||
|
upper_part.append("<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % (location["base_url"], location["name"]))
|
||||||
|
upper_part.append("</div>")
|
||||||
|
upper_part.append("<hr align=\"center\" width=\"100%\">")
|
||||||
|
|
||||||
|
return upper_part
|
||||||
|
|
||||||
def pageend(self):
|
def pageend(self):
|
||||||
return [
|
return [
|
||||||
@ -76,3 +85,21 @@ class fHDHR_Pages():
|
|||||||
"}",
|
"}",
|
||||||
"</script>"
|
"</script>"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class fHDHR_Pages():
|
||||||
|
|
||||||
|
def __init__(self, settings, device):
|
||||||
|
self.config = settings
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
self.page_elements = fHDHR_Page_Elements(settings, device)
|
||||||
|
|
||||||
|
self.htmlerror = HTMLerror(settings)
|
||||||
|
|
||||||
|
self.index = Index_HTML(settings, self.device, self.page_elements)
|
||||||
|
self.origin = Origin_HTML(settings, self.device, self.page_elements)
|
||||||
|
self.cluster = Cluster_HTML(settings, self.device, self.page_elements)
|
||||||
|
self.diagnostics = Diagnostics_HTML(settings, self.device, self.page_elements)
|
||||||
|
self.version = Version_HTML(settings, self.device, self.page_elements)
|
||||||
|
self.channel_guide = Channel_Guide_HTML(settings, self.device, self.page_elements)
|
||||||
|
|||||||
@ -18,8 +18,9 @@ class Channel_Guide_HTML():
|
|||||||
nowtime = datetime.datetime.utcnow()
|
nowtime = datetime.datetime.utcnow()
|
||||||
|
|
||||||
fakefile = StringIO()
|
fakefile = StringIO()
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
for line in self.page_elements["top"]:
|
for line in page_elements["top"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
fakefile.write("<h4 id=\"mcetoc_1cdobsl3g0\" style=\"text-align: center;\"><span style=\"text-decoration: underline;\"><strong><em>What's On %s</em></strong></span></h4>\n" % friendlyname)
|
fakefile.write("<h4 id=\"mcetoc_1cdobsl3g0\" style=\"text-align: center;\"><span style=\"text-decoration: underline;\"><strong><em>What's On %s</em></strong></span></h4>\n" % friendlyname)
|
||||||
@ -53,7 +54,7 @@ class Channel_Guide_HTML():
|
|||||||
fakefile.write(" <td>%s</td>\n" % (str(remaining_time)))
|
fakefile.write(" <td>%s</td>\n" % (str(remaining_time)))
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
for line in self.page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
channel_guide_html = fakefile.getvalue()
|
channel_guide_html = fakefile.getvalue()
|
||||||
|
|||||||
80
fHDHR/api/hub/pages/cluster_html.py
Normal file
80
fHDHR/api/hub/pages/cluster_html.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from io import StringIO
|
||||||
|
import urllib.parse
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Cluster_HTML():
|
||||||
|
|
||||||
|
def __init__(self, settings, device, page_elements):
|
||||||
|
self.config = settings
|
||||||
|
self.device = device
|
||||||
|
self.page_elements = page_elements
|
||||||
|
|
||||||
|
def get_cluster_html(self, base_url, force_update=False):
|
||||||
|
|
||||||
|
fakefile = StringIO()
|
||||||
|
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
|
for line in page_elements["top"]:
|
||||||
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
|
fakefile.write("<h4 style=\"text-align: center;\">Cluster</h4>")
|
||||||
|
fakefile.write("\n")
|
||||||
|
|
||||||
|
fakefile.write("<div style=\"text-align: center;\">\n")
|
||||||
|
fakefile.write(" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" % ("/cluster?method=scan", "Force Scan"))
|
||||||
|
fakefile.write(" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" % ("/cluster?method=disconnect", "Disconnect"))
|
||||||
|
fakefile.write("</div><br>\n")
|
||||||
|
|
||||||
|
fakefile.write("<table class=\"center\" style=\"width:50%\">\n")
|
||||||
|
fakefile.write(" <tr>\n")
|
||||||
|
fakefile.write(" <th>Name</th>\n")
|
||||||
|
fakefile.write(" <th>Location</th>\n")
|
||||||
|
fakefile.write(" <th>Joined</th>\n")
|
||||||
|
fakefile.write(" <th>Options</th>\n")
|
||||||
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
|
fhdhr_list = self.device.cluster.get_list()
|
||||||
|
for location in list(fhdhr_list.keys()):
|
||||||
|
fakefile.write(" <tr>\n")
|
||||||
|
|
||||||
|
if location in list(self.device.cluster.cluster.keys()):
|
||||||
|
location_name = self.device.cluster.cluster[location]["name"]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
location_info_url = location + "/discover.json"
|
||||||
|
locatation_info_req = requests.get(location_info_url)
|
||||||
|
location_info = locatation_info_req.json()
|
||||||
|
location_name = location_info["FriendlyName"]
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("Unreachable: " + location)
|
||||||
|
fakefile.write(" <td>%s</td>\n" % (str(location_name)))
|
||||||
|
|
||||||
|
fakefile.write(" <td>%s</td>\n" % (str(location)))
|
||||||
|
|
||||||
|
fakefile.write(" <td>%s</td>\n" % (str(fhdhr_list[location]["Joined"])))
|
||||||
|
|
||||||
|
fakefile.write(" <td>\n")
|
||||||
|
fakefile.write(" <div>\n")
|
||||||
|
location_url_query = urllib.parse.quote(location)
|
||||||
|
fakefile.write(
|
||||||
|
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
|
||||||
|
(location, "Visit"))
|
||||||
|
if not fhdhr_list[location]["Joined"]:
|
||||||
|
fakefile.write(
|
||||||
|
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
|
||||||
|
("/cluster?method=add&location=" + location_url_query, "Add"))
|
||||||
|
else:
|
||||||
|
fakefile.write(
|
||||||
|
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
|
||||||
|
("/cluster?method=del&location=" + location_url_query, "Remove"))
|
||||||
|
fakefile.write(" </div>\n")
|
||||||
|
fakefile.write(" </td>\n")
|
||||||
|
|
||||||
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
|
for line in page_elements["end"]:
|
||||||
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
|
return fakefile.getvalue()
|
||||||
@ -13,8 +13,9 @@ class Diagnostics_HTML():
|
|||||||
if not self.diagnostics_html or force_update:
|
if not self.diagnostics_html or force_update:
|
||||||
|
|
||||||
fakefile = StringIO()
|
fakefile = StringIO()
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
for line in self.page_elements["top"]:
|
for line in page_elements["top"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
# a list of 2 part lists containing button information
|
# a list of 2 part lists containing button information
|
||||||
@ -24,7 +25,8 @@ class Diagnostics_HTML():
|
|||||||
["device.xml", "device.xml"],
|
["device.xml", "device.xml"],
|
||||||
["discover.json", "discover.json"],
|
["discover.json", "discover.json"],
|
||||||
["lineup.json", "lineup.json"],
|
["lineup.json", "lineup.json"],
|
||||||
["lineup_status.json", "lineup_status.json"]
|
["lineup_status.json", "lineup_status.json"],
|
||||||
|
["cluster.json", "cluster.json"]
|
||||||
]
|
]
|
||||||
|
|
||||||
for button_item in button_list:
|
for button_item in button_list:
|
||||||
@ -35,7 +37,7 @@ class Diagnostics_HTML():
|
|||||||
fakefile.write("</div>\n")
|
fakefile.write("</div>\n")
|
||||||
fakefile.write("\n")
|
fakefile.write("\n")
|
||||||
|
|
||||||
for line in self.page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
self.diagnostics_html = fakefile.getvalue()
|
self.diagnostics_html = fakefile.getvalue()
|
||||||
|
|||||||
@ -11,8 +11,9 @@ class Index_HTML():
|
|||||||
def get_index_html(self, base_url, force_update=False):
|
def get_index_html(self, base_url, force_update=False):
|
||||||
|
|
||||||
fakefile = StringIO()
|
fakefile = StringIO()
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
for line in self.page_elements["top"]:
|
for line in page_elements["top"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
fakefile.write("<h4 style=\"text-align: center;\">fHDHR Status</h4>")
|
fakefile.write("<h4 style=\"text-align: center;\">fHDHR Status</h4>")
|
||||||
@ -43,7 +44,7 @@ class Index_HTML():
|
|||||||
fakefile.write(" <td>%s</td>\n" % (guts[1]))
|
fakefile.write(" <td>%s</td>\n" % (guts[1]))
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
for line in self.page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
return fakefile.getvalue()
|
return fakefile.getvalue()
|
||||||
|
|||||||
@ -13,8 +13,9 @@ class Origin_HTML():
|
|||||||
servicename = str(self.config.dict["main"]["servicename"])
|
servicename = str(self.config.dict["main"]["servicename"])
|
||||||
|
|
||||||
fakefile = StringIO()
|
fakefile = StringIO()
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
for line in self.page_elements["top"]:
|
for line in page_elements["top"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
fakefile.write("<h4 style=\"text-align: center;\">%s Status</h4>" % (servicename))
|
fakefile.write("<h4 style=\"text-align: center;\">%s Status</h4>" % (servicename))
|
||||||
@ -33,7 +34,7 @@ class Origin_HTML():
|
|||||||
fakefile.write(" <td>%s</td>\n" % (str(origin_status_dict[key])))
|
fakefile.write(" <td>%s</td>\n" % (str(origin_status_dict[key])))
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
for line in self.page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
return fakefile.getvalue()
|
return fakefile.getvalue()
|
||||||
|
|||||||
@ -13,8 +13,9 @@ class Version_HTML():
|
|||||||
def get_version_html(self, base_url, force_update=False):
|
def get_version_html(self, base_url, force_update=False):
|
||||||
|
|
||||||
fakefile = StringIO()
|
fakefile = StringIO()
|
||||||
|
page_elements = self.page_elements.get()
|
||||||
|
|
||||||
for line in self.page_elements["top"]:
|
for line in page_elements["top"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
fakefile.write("<table class=\"center\" style=\"width:50%\">\n")
|
fakefile.write("<table class=\"center\" style=\"width:50%\">\n")
|
||||||
@ -28,7 +29,7 @@ class Version_HTML():
|
|||||||
fakefile.write(" <td>%s</td>\n" % (str(fHDHR_VERSION)))
|
fakefile.write(" <td>%s</td>\n" % (str(fHDHR_VERSION)))
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
for line in self.page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
fakefile.write(line + "\n")
|
fakefile.write(line + "\n")
|
||||||
|
|
||||||
return fakefile.getvalue()
|
return fakefile.getvalue()
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import fHDHR.config
|
|||||||
|
|
||||||
import fHDHR.origin
|
import fHDHR.origin
|
||||||
import fHDHR.api
|
import fHDHR.api
|
||||||
import fHDHR.ssdpserver
|
|
||||||
|
|
||||||
ERR_CODE = 1
|
ERR_CODE = 1
|
||||||
ERR_CODE_NO_RESTART = 2
|
ERR_CODE_NO_RESTART = 2
|
||||||
@ -37,10 +36,6 @@ def get_configuration(args, script_dir):
|
|||||||
|
|
||||||
def run(settings, origin):
|
def run(settings, origin):
|
||||||
|
|
||||||
if settings.dict["fhdhr"]["discovery_address"]:
|
|
||||||
ssdpServer = Process(target=fHDHR.ssdpserver.ssdpServerProcess, args=(settings,))
|
|
||||||
ssdpServer.start()
|
|
||||||
|
|
||||||
fhdhrweb = Process(target=fHDHR.api.interface_start, args=(settings, origin))
|
fhdhrweb = Process(target=fHDHR.api.interface_start, args=(settings, origin))
|
||||||
fhdhrweb.start()
|
fhdhrweb.start()
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,8 @@ class Config():
|
|||||||
cache_dir = self.dict["filedir"]["cache_dir"]
|
cache_dir = self.dict["filedir"]["cache_dir"]
|
||||||
|
|
||||||
self.dict["main"]["channel_numbers"] = pathlib.Path(cache_dir).joinpath("cnumbers.json")
|
self.dict["main"]["channel_numbers"] = pathlib.Path(cache_dir).joinpath("cnumbers.json")
|
||||||
|
self.dict["main"]["ssdp_detect"] = pathlib.Path(cache_dir).joinpath("ssdp_list.json")
|
||||||
|
self.dict["main"]["cluster"] = pathlib.Path(cache_dir).joinpath("cluster.json")
|
||||||
|
|
||||||
for epg_method in self.dict["main"]["valid_epg_methods"]:
|
for epg_method in self.dict["main"]["valid_epg_methods"]:
|
||||||
if epg_method and epg_method != "None":
|
if epg_method and epg_method != "None":
|
||||||
|
|||||||
@ -1,242 +0,0 @@
|
|||||||
# Licensed under the MIT license
|
|
||||||
# http://opensource.org/licenses/mit-license.php
|
|
||||||
|
|
||||||
# Copyright 2005, Tim Potter <tpot@samba.org>
|
|
||||||
# Copyright 2006 John-Mark Gurney <gurney_j@resnet.uroegon.edu>
|
|
||||||
# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com).
|
|
||||||
# Copyright 2006,2007,2008,2009 Frank Scholz <coherence@beebits.net>
|
|
||||||
# Copyright 2016 Erwan Martin <public@fzwte.net>
|
|
||||||
#
|
|
||||||
# Implementation of a SSDP server.
|
|
||||||
#
|
|
||||||
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
import socket
|
|
||||||
import logging
|
|
||||||
from email.utils import formatdate
|
|
||||||
from errno import ENOPROTOOPT
|
|
||||||
|
|
||||||
SSDP_ADDR = '239.255.255.250'
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
# mostly from https://github.com/ZeWaren/python-upnp-ssdp-example
|
|
||||||
def ssdpServerProcess(settings):
|
|
||||||
ssdp = SSDPServer()
|
|
||||||
ssdp.ssdp_port = 1900
|
|
||||||
ssdp.register('local',
|
|
||||||
'uuid:' + settings.dict["main"]["uuid"] + '::upnp:rootdevice',
|
|
||||||
'upnp:rootdevice',
|
|
||||||
'http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
|
|
||||||
str(settings.dict["fhdhr"]["port"]) + '/device.xml')
|
|
||||||
print("SSDP server Started on port " + str(ssdp.ssdp_port) +
|
|
||||||
" and broadcasting the availability of " + settings.dict["fhdhr"]["friendlyname"] +
|
|
||||||
" at " 'http://' + settings.dict["fhdhr"]["discovery_address"] + ':' + str(settings.dict["fhdhr"]["port"]))
|
|
||||||
try:
|
|
||||||
ssdp.run()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SSDPServer:
|
|
||||||
"""A class implementing a SSDP server. The notify_received and
|
|
||||||
searchReceived methods are called when the appropriate type of
|
|
||||||
datagram is received by the server."""
|
|
||||||
known = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
if hasattr(socket, "SO_REUSEPORT"):
|
|
||||||
try:
|
|
||||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
||||||
except socket.error as le:
|
|
||||||
# RHEL6 defines SO_REUSEPORT but it doesn't work
|
|
||||||
if le.errno == ENOPROTOOPT:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
addr = socket.inet_aton(SSDP_ADDR)
|
|
||||||
interface = socket.inet_aton('0.0.0.0')
|
|
||||||
cmd = socket.IP_ADD_MEMBERSHIP
|
|
||||||
self.sock.setsockopt(socket.IPPROTO_IP, cmd, addr + interface)
|
|
||||||
self.sock.bind(('0.0.0.0', self.ssdp_port))
|
|
||||||
self.sock.settimeout(1)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data, addr = self.sock.recvfrom(1024)
|
|
||||||
self.datagram_received(data, addr)
|
|
||||||
except socket.timeout:
|
|
||||||
continue
|
|
||||||
self.shutdown()
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
for st in self.known:
|
|
||||||
if self.known[st]['MANIFESTATION'] == 'local':
|
|
||||||
self.do_byebye(st)
|
|
||||||
|
|
||||||
def datagram_received(self, data, host_port):
|
|
||||||
"""Handle a received multicast datagram."""
|
|
||||||
|
|
||||||
(host, port) = host_port
|
|
||||||
|
|
||||||
try:
|
|
||||||
header, payload = data.decode().split('\r\n\r\n')[:2]
|
|
||||||
except ValueError as err:
|
|
||||||
logger.error(err)
|
|
||||||
return
|
|
||||||
|
|
||||||
lines = header.split('\r\n')
|
|
||||||
cmd = lines[0].split(' ')
|
|
||||||
lines = [x.replace(': ', ':', 1) for x in lines[1:]]
|
|
||||||
lines = [x for x in lines if len(x) > 0]
|
|
||||||
|
|
||||||
headers = [x.split(':', 1) for x in lines]
|
|
||||||
headers = dict([(x[0].lower(), x[1]) for x in headers])
|
|
||||||
|
|
||||||
logger.info('SSDP command %s %s - from %s:%d' % (cmd[0], cmd[1], host, port))
|
|
||||||
logger.debug('with headers: {}.'.format(headers))
|
|
||||||
if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
|
|
||||||
# SSDP discovery
|
|
||||||
self.discovery_request(headers, (host, port))
|
|
||||||
elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
|
|
||||||
# SSDP presence
|
|
||||||
logger.debug('NOTIFY *')
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))
|
|
||||||
|
|
||||||
def register(self, manifestation, usn, st, location, cache_control='max-age=1800', silent=False,
|
|
||||||
host=None):
|
|
||||||
"""Register a service or device that this SSDP server will
|
|
||||||
respond to."""
|
|
||||||
|
|
||||||
logging.info('Registering %s (%s)' % (st, location))
|
|
||||||
|
|
||||||
self.known[usn] = {}
|
|
||||||
self.known[usn]['USN'] = usn
|
|
||||||
self.known[usn]['LOCATION'] = location
|
|
||||||
self.known[usn]['ST'] = st
|
|
||||||
self.known[usn]['EXT'] = ''
|
|
||||||
self.known[usn]['SERVER'] = "fHDHR Server"
|
|
||||||
self.known[usn]['CACHE-CONTROL'] = cache_control
|
|
||||||
|
|
||||||
self.known[usn]['MANIFESTATION'] = manifestation
|
|
||||||
self.known[usn]['SILENT'] = silent
|
|
||||||
self.known[usn]['HOST'] = host
|
|
||||||
self.known[usn]['last-seen'] = time.time()
|
|
||||||
|
|
||||||
if manifestation == 'local' and self.sock:
|
|
||||||
self.do_notify(usn)
|
|
||||||
|
|
||||||
def unregister(self, usn):
|
|
||||||
logger.info("Un-registering %s" % usn)
|
|
||||||
del self.known[usn]
|
|
||||||
|
|
||||||
def is_known(self, usn):
|
|
||||||
return usn in self.known
|
|
||||||
|
|
||||||
def send_it(self, response, destination, delay, usn):
|
|
||||||
logger.debug('send discovery response delayed by %ds for %s to %r' % (delay, usn, destination))
|
|
||||||
try:
|
|
||||||
self.sock.sendto(response.encode(), destination)
|
|
||||||
except (AttributeError, socket.error) as msg:
|
|
||||||
logger.warning("failure sending out byebye notification: %r" % msg)
|
|
||||||
|
|
||||||
def discovery_request(self, headers, host_port):
|
|
||||||
"""Process a discovery request. The response must be sent to
|
|
||||||
the address specified by (host, port)."""
|
|
||||||
|
|
||||||
(host, port) = host_port
|
|
||||||
|
|
||||||
logger.info('Discovery request from (%s,%d) for %s' % (host, port, headers['st']))
|
|
||||||
logger.info('Discovery request for %s' % headers['st'])
|
|
||||||
|
|
||||||
# Do we know about this service?
|
|
||||||
for i in list(self.known.values()):
|
|
||||||
if i['MANIFESTATION'] == 'remote':
|
|
||||||
continue
|
|
||||||
if headers['st'] == 'ssdp:all' and i['SILENT']:
|
|
||||||
continue
|
|
||||||
if i['ST'] == headers['st'] or headers['st'] == 'ssdp:all':
|
|
||||||
response = ['HTTP/1.1 200 OK']
|
|
||||||
|
|
||||||
usn = None
|
|
||||||
for k, v in list(i.items()):
|
|
||||||
if k == 'USN':
|
|
||||||
usn = v
|
|
||||||
if k not in ('MANIFESTATION', 'SILENT', 'HOST'):
|
|
||||||
response.append('%s: %s' % (k, v))
|
|
||||||
|
|
||||||
if usn:
|
|
||||||
response.append('DATE: %s' % formatdate(timeval=None, localtime=False, usegmt=True))
|
|
||||||
|
|
||||||
response.extend(('', ''))
|
|
||||||
delay = random.randint(0, int(headers['mx']))
|
|
||||||
|
|
||||||
self.send_it('\r\n'.join(response), (host, port), delay, usn)
|
|
||||||
|
|
||||||
def do_notify(self, usn):
|
|
||||||
"""Do notification"""
|
|
||||||
|
|
||||||
if self.known[usn]['SILENT']:
|
|
||||||
return
|
|
||||||
logger.info('Sending alive notification for %s' % usn)
|
|
||||||
|
|
||||||
resp = [
|
|
||||||
'NOTIFY * HTTP/1.1',
|
|
||||||
'HOST: %s:%d' % (SSDP_ADDR, self.ssdp_port),
|
|
||||||
'NTS: ssdp:alive',
|
|
||||||
]
|
|
||||||
stcpy = dict(list(self.known[usn].items()))
|
|
||||||
stcpy['NT'] = stcpy['ST']
|
|
||||||
del stcpy['ST']
|
|
||||||
del stcpy['MANIFESTATION']
|
|
||||||
del stcpy['SILENT']
|
|
||||||
del stcpy['HOST']
|
|
||||||
del stcpy['last-seen']
|
|
||||||
|
|
||||||
resp.extend([': '.join(x) for x in list(stcpy.items())])
|
|
||||||
resp.extend(('', ''))
|
|
||||||
logger.debug('do_notify content', resp)
|
|
||||||
try:
|
|
||||||
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, self.ssdp_port))
|
|
||||||
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, self.ssdp_port))
|
|
||||||
except (AttributeError, socket.error) as msg:
|
|
||||||
logger.warning("failure sending out alive notification: %r" % msg)
|
|
||||||
|
|
||||||
def do_byebye(self, usn):
|
|
||||||
"""Do byebye"""
|
|
||||||
|
|
||||||
logger.info('Sending byebye notification for %s' % usn)
|
|
||||||
|
|
||||||
resp = [
|
|
||||||
'NOTIFY * HTTP/1.1',
|
|
||||||
'HOST: %s:%d' % (SSDP_ADDR, self.ssdp_port),
|
|
||||||
'NTS: ssdp:byebye',
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
stcpy = dict(list(self.known[usn].items()))
|
|
||||||
stcpy['NT'] = stcpy['ST']
|
|
||||||
del stcpy['ST']
|
|
||||||
del stcpy['MANIFESTATION']
|
|
||||||
del stcpy['SILENT']
|
|
||||||
del stcpy['HOST']
|
|
||||||
del stcpy['last-seen']
|
|
||||||
resp.extend([': '.join(x) for x in list(stcpy.items())])
|
|
||||||
resp.extend(('', ''))
|
|
||||||
logger.debug('do_byebye content', resp)
|
|
||||||
if self.sock:
|
|
||||||
try:
|
|
||||||
self.sock.sendto('\r\n'.join(resp), (SSDP_ADDR, self.ssdp_port))
|
|
||||||
except (AttributeError, socket.error) as msg:
|
|
||||||
logger.error("failure sending out byebye notification: %r" % msg)
|
|
||||||
except KeyError as msg:
|
|
||||||
logger.error("error building byebye notification: %r" % msg)
|
|
||||||
Loading…
Reference in New Issue
Block a user