1
0
mirror of https://github.com/fHDHR/fHDHR_NextPVR.git synced 2025-12-06 14:46:58 -05:00

Implement Plex Remote Media Grabber

This commit is contained in:
deathbybandaid 2020-12-04 08:25:48 -05:00
parent d4dacc5f3b
commit 34ca98881f
56 changed files with 1234 additions and 344 deletions

View File

@ -0,0 +1,39 @@
{
"database":{
"type":{
"value": "sqlite",
"config_file": true,
"config_web": false
},
"driver":{
"value": "none",
"config_file": true,
"config_web": false
},
"user":{
"value": "none",
"config_file": true,
"config_web": false
},
"pass":{
"value": "none",
"config_file": true,
"config_web": false
},
"host":{
"value": "none",
"config_file": true,
"config_web": false
},
"port":{
"value": "none",
"config_file": true,
"config_web": false
},
"name":{
"value": "none",
"config_file": true,
"config_web": false
}
}
}

View File

@ -0,0 +1,9 @@
{
"epg":{
"images":{
"value": "pass",
"config_file": true,
"config_web": true
}
}
}

View File

@ -1,21 +1,4 @@
{ {
"main":{
"uuid":{
"value": "none",
"config_file": true,
"config_web": false
},
"cache_dir":{
"value": "none",
"config_file": true,
"config_web": true
},
"thread_method":{
"value": "multiprocessing",
"config_file": true,
"config_web": true
}
},
"fhdhr":{ "fhdhr":{
"address":{ "address":{
"value": "0.0.0.0", "value": "0.0.0.0",
@ -74,87 +57,5 @@
"config_file": true, "config_file": true,
"config_web": true "config_web": true
} }
},
"epg":{
"images":{
"value": "pass",
"config_file": true,
"config_web": true
}
},
"ffmpeg":{
"path":{
"value": "ffmpeg",
"config_file": true,
"config_web": true
},
"bytes_per_read":{
"value": 1152000,
"config_file": true,
"config_web": true
}
},
"vlc":{
"path":{
"value": "cvlc",
"config_file": true,
"config_web": true
},
"bytes_per_read":{
"value": 1152000,
"config_file": true,
"config_web": true
}
},
"direct_stream":{
"chunksize":{
"value": 1048576,
"config_file": true,
"config_web": true
}
},
"logging":{
"level":{
"value": "WARNING",
"config_file": true,
"config_web": true
}
},
"database":{
"type":{
"value": "sqlite",
"config_file": true,
"config_web": false
},
"driver":{
"value": "none",
"config_file": true,
"config_web": false
},
"user":{
"value": "none",
"config_file": true,
"config_web": false
},
"pass":{
"value": "none",
"config_file": true,
"config_web": false
},
"host":{
"value": "none",
"config_file": true,
"config_web": false
},
"port":{
"value": "none",
"config_file": true,
"config_web": false
},
"name":{
"value": "none",
"config_file": true,
"config_web": false
}
} }
} }

View File

@ -0,0 +1,9 @@
{
"logging":{
"level":{
"value": "WARNING",
"config_file": true,
"config_web": true
}
}
}

View File

@ -0,0 +1,19 @@
{
"main":{
"uuid":{
"value": "none",
"config_file": true,
"config_web": false
},
"cache_dir":{
"value": "none",
"config_file": true,
"config_web": true
},
"thread_method":{
"value": "multiprocessing",
"config_file": true,
"config_web": true
}
}
}

View File

@ -0,0 +1,9 @@
{
"rmg":{
"enabled":{
"value": true,
"config_file": true,
"config_web": false
}
}
}

View File

@ -0,0 +1,33 @@
{
"ffmpeg":{
"path":{
"value": "ffmpeg",
"config_file": true,
"config_web": true
},
"bytes_per_read":{
"value": 1152000,
"config_file": true,
"config_web": true
}
},
"vlc":{
"path":{
"value": "cvlc",
"config_file": true,
"config_web": true
},
"bytes_per_read":{
"value": 1152000,
"config_file": true,
"config_web": true
}
},
"direct_stream":{
"chunksize":{
"value": 1048576,
"config_file": true,
"config_web": true
}
}
}

View File

@ -22,7 +22,7 @@
<button class="pull-left" onclick="OpenLink('/channels')">Channels</a></button> <button class="pull-left" onclick="OpenLink('/channels')">Channels</a></button>
<button class="pull-left" onclick="OpenLink('/guide')">Guide</a></button> <button class="pull-left" onclick="OpenLink('/guide')">Guide</a></button>
<button class="pull-left" onclick="OpenLink('/cluster')">Cluster</a></button> <button class="pull-left" onclick="OpenLink('/cluster')">Cluster</a></button>
<button class="pull-left" onclick="OpenLink('/streams')">Streams</a></button> <button class="pull-left" onclick="OpenLink('/tuners')">Tuners</a></button>
<button class="pull-left" onclick="OpenLink('/xmltv')">xmltv</a></button> <button class="pull-left" onclick="OpenLink('/xmltv')">xmltv</a></button>
<button class="pull-left" onclick="OpenLink('/version')">Version</a></button> <button class="pull-left" onclick="OpenLink('/version')">Version</a></button>
<button class="pull-left" onclick="OpenLink('/diagnostics')">Diagnostics</a></button> <button class="pull-left" onclick="OpenLink('/diagnostics')">Diagnostics</a></button>

View File

@ -5,7 +5,7 @@
<h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels</h4> <h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels</h4>
<div style="text-align: center;"> <div style="text-align: center;">
<button onclick="OpenLink('/api/channels?method=scan&redirect=%2Fchannels')">Force Channel Update</a></button><p> Note: This may take some time.</p> <button onclick="OpenLink('/api/tuners?method=scan&redirect=%2Fchannels')">Force Channel Update</a></button><p> Note: This may take some time.</p>
</div> </div>
<br> <br>

View File

@ -2,10 +2,35 @@
{% block content %} {% block content %}
<h4 style="text-align: center;">fHDHR Diagnostic Links</h4>
<table class="center" style="width:100%">
<tr>
<th>Item</th>
<th>HDHR</th>
<th>RMG</th>
<th>Non-Specific</th>
</tr>
{% for button_item in button_list %} {% for button_item in button_list %}
<div style="text-align: center;"> <tr>
<p><button onclick="OpenLink('{{ button_item[1] }}')">{{ button_item[0] }}</a></button></p> <td>{{ button_item["label"] }}</td>
</div> {% if button_item["hdhr"] %}
<td><button onclick="OpenLink('{{ button_item["hdhr"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
{% if button_item["rmg"] %}
<td><button onclick="OpenLink('{{ button_item["rmg"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
{% if button_item["other"] %}
<td><button onclick="OpenLink('{{ button_item["other"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
</tr>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -19,8 +19,12 @@
<tr> <tr>
<td>{{ tuner_dict["number"] }}</td> <td>{{ tuner_dict["number"] }}</td>
<td>{{ tuner_dict["status"] }}</td> <td>{{ tuner_dict["status"] }}</td>
{% if tuner_dict["status"] == "Active" %} {% if tuner_dict["status"] in ["Active", "Acquired"] %}
<td>{{ tuner_dict["channel_number"] }}</td> <td>{{ tuner_dict["channel_number"] }}</td>
{% else %}
<td>N/A</td>
{% endif %}
{% if tuner_dict["status"] == "Active" %}
<td>{{ tuner_dict["method"] }}</td> <td>{{ tuner_dict["method"] }}</td>
<td>{{ tuner_dict["play_duration"] }}</td> <td>{{ tuner_dict["play_duration"] }}</td>
<td>{{ tuner_dict["downloaded"] }}</td> <td>{{ tuner_dict["downloaded"] }}</td>
@ -28,12 +32,14 @@
<td>N/A</td> <td>N/A</td>
<td>N/A</td> <td>N/A</td>
<td>N/A</td> <td>N/A</td>
<td>N/A</td>
{% endif %} {% endif %}
<td> <td>
<div> <div>
{% if tuner_dict["status"] in ["Active", "Acquired"] %} {% if tuner_dict["status"] != "Inactive" %}
<button onclick="OpenLink('/api/watch?method=close&tuner={{ tuner_dict["number"] }}&redirect=%2Fstreams')">Close</a></button> <button onclick="OpenLink('/api/tuners?method=close&tuner={{ tuner_dict["number"] }}&redirect=%2Ftuners')">Close</a></button>
{% endif %}
{% if not tuner_scanning and tuner_dict["status"] == "Inactive" %}
<button onclick="OpenLink('/api/tuners?method=scan&tuner={{ tuner_dict["number"] }}&redirect=%2Ftuners')">Channel Scan</a></button>
{% endif %} {% endif %}
</div> </div>
</td> </td>

View File

@ -2,7 +2,6 @@ from .channels import Channels
from .epg import EPG from .epg import EPG
from .tuners import Tuners from .tuners import Tuners
from .images import imageHandler from .images import imageHandler
from .station_scan import Station_Scan
from .ssdp import SSDPServer from .ssdp import SSDPServer
from .cluster import fHDHR_Cluster from .cluster import fHDHR_Cluster
@ -19,8 +18,6 @@ class fHDHR_Device():
self.images = imageHandler(fhdhr, self.epg) self.images = imageHandler(fhdhr, self.epg)
self.station_scan = Station_Scan(fhdhr, self.channels)
self.ssdp = SSDPServer(fhdhr) self.ssdp = SSDPServer(fhdhr)
self.cluster = fHDHR_Cluster(fhdhr, self.ssdp) self.cluster = fHDHR_Cluster(fhdhr, self.ssdp)

View File

@ -124,7 +124,7 @@ class fHDHR_Cluster():
self.fhdhr.logger.info("Adding %s to cluster." % location) self.fhdhr.logger.info("Adding %s to cluster." % location)
cluster[location] = {"base_url": location} cluster[location] = {"base_url": location}
location_info_url = location + "/discover.json" location_info_url = "%s/hdhr/discover.json" % location
try: try:
location_info_req = self.fhdhr.web.session.get(location_info_url) location_info_req = self.fhdhr.web.session.get(location_info_url)
except self.fhdhr.web.exceptions.ConnectionError: except self.fhdhr.web.exceptions.ConnectionError:

View File

@ -2,21 +2,9 @@
import socket import socket
import struct import struct
from .ssdp_detect import fHDHR_Detect
class fHDHR_Detect(): from .rmg_ssdp import RMG_SSDP
from .hdhr_ssdp import HDHR_SSDP
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.fhdhr.db.delete_fhdhr_value("ssdp_detect", "list")
def set(self, location):
detect_list = self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
if location not in detect_list:
detect_list.append(location)
self.fhdhr.db.set_fhdhr_value("ssdp_detect", "list", detect_list)
def get(self):
return self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
class SSDPServer(): class SSDPServer():
@ -33,18 +21,14 @@ class SSDPServer():
self.port = 1900 self.port = 1900
self.iface = None self.iface = None
self.address = None self.address = None
self.server = 'fHDHR/%s UPnP/1.0' % fhdhr.version
allowed_protos = ("ipv4", "ipv6") allowed_protos = ("ipv4", "ipv6")
if self.proto not in allowed_protos: if self.proto not in allowed_protos:
raise ValueError("Invalid proto - expected one of {}".format(allowed_protos)) raise ValueError("Invalid proto - expected one of {}".format(allowed_protos))
self.nt = 'urn:schemas-upnp-org:device:MediaServer:1'
self.usn = 'uuid:' + fhdhr.config.dict["main"]["uuid"] + '::' + self.nt
self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' + self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' +
str(fhdhr.config.dict["fhdhr"]["port"]) + '/device.xml') str(fhdhr.config.dict["fhdhr"]["port"]) + '/device.xml')
self.al = self.location
self.max_age = 1800
self._iface = None self._iface = None
if self.proto == "ipv4": if self.proto == "ipv4":
@ -95,9 +79,11 @@ class SSDPServer():
self.sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1) self.sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
self.sock.bind((self.bind_address, self.port)) self.sock.bind((self.bind_address, self.port))
self.notify_payload = self.create_notify_payload()
self.msearch_payload = self.create_msearch_payload() self.msearch_payload = self.create_msearch_payload()
self.rmg_ssdp = RMG_SSDP(fhdhr, self._broadcast_ip)
self.hdhr_ssdp = HDHR_SSDP(fhdhr, self._broadcast_ip)
self.m_search() self.m_search()
def on_recv(self, data, address): def on_recv(self, data, address):
@ -123,7 +109,18 @@ class SSDPServer():
# SSDP discovery # SSDP discovery
self.fhdhr.logger.debug("Received qualifying M-SEARCH from {}".format(address)) self.fhdhr.logger.debug("Received qualifying M-SEARCH from {}".format(address))
self.fhdhr.logger.debug("M-SEARCH data: {}".format(headers)) self.fhdhr.logger.debug("M-SEARCH data: {}".format(headers))
notify = self.notify_payload
notify_list = []
hdhr_notify = self.hdhr_ssdp.get()
notify_list.append(hdhr_notify)
if self.fhdhr.config.dict["rmg"]["enabled"]:
rmg_notify = self.rmg_ssdp.get()
notify_list.append(rmg_notify)
for notify in notify_list:
self.fhdhr.logger.debug("Created NOTIFY: {}".format(notify)) self.fhdhr.logger.debug("Created NOTIFY: {}".format(notify))
try: try:
self.sock.sendto(notify, address) self.sock.sendto(notify, address)
@ -137,7 +134,8 @@ class SSDPServer():
try: try:
if headers["server"].startswith("fHDHR"): if headers["server"].startswith("fHDHR"):
if headers["location"] != self.location: if headers["location"] != self.location:
self.detect_method.set(headers["location"].split("/device.xml")[0]) savelocation = headers["location"].split("/device.xml")[0]
self.detect_method.set(savelocation)
except KeyError: except KeyError:
return return
else: else:
@ -147,31 +145,6 @@ class SSDPServer():
data = self.msearch_payload data = self.msearch_payload
self.sock.sendto(data, self._address) 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): def create_msearch_payload(self):
data = ( data = (
"M-SEARCH * HTTP/1.1\r\n" "M-SEARCH * HTTP/1.1\r\n"

View File

@ -0,0 +1,44 @@
class HDHR_SSDP():
def __init__(self, fhdhr, _broadcast_ip):
self.fhdhr = fhdhr
self.ssdp_content = None
self._broadcast_ip = _broadcast_ip
self.nt = 'urn:schemas-upnp-org:device:MediaServer:1'
self.usn = 'uuid:' + fhdhr.config.dict["main"]["uuid"] + '::' + self.nt
self.server = 'fHDHR/%s UPnP/1.0' % fhdhr.version
self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' +
str(fhdhr.config.dict["fhdhr"]["port"]) + '/device.xml')
self.al = self.location
self.max_age = 1800
def get(self):
if self.ssdp_content:
return self.ssdp_content.encode("utf-8")
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"
self.ssdp_content = data
return data.encode("utf-8")

View File

@ -0,0 +1,44 @@
class RMG_SSDP():
def __init__(self, fhdhr, _broadcast_ip):
self.fhdhr = fhdhr
self.ssdp_content = None
self._broadcast_ip = _broadcast_ip
self.nt = 'urn:schemas-upnp-org:device-1-0'
self.usn = 'uuid:' + fhdhr.config.dict["main"]["uuid"] + '::' + self.nt
self.server = 'fHDHR/%s UPnP/1.0' % fhdhr.version
self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' +
str(fhdhr.config.dict["fhdhr"]["port"]) + '/device.xml')
self.al = self.location
self.max_age = 1800
def get(self):
if self.ssdp_content:
return self.ssdp_content.encode("utf-8")
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"
self.ssdp_content = data
return data.encode("utf-8")

View File

@ -0,0 +1,16 @@
class fHDHR_Detect():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.fhdhr.db.delete_fhdhr_value("ssdp_detect", "list")
def set(self, location):
detect_list = self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
if location not in detect_list:
detect_list.append(location)
self.fhdhr.db.set_fhdhr_value("ssdp_detect", "list", detect_list)
def get(self):
return self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []

View File

@ -1,43 +0,0 @@
import multiprocessing
import threading
class Station_Scan():
def __init__(self, fhdhr, channels):
self.fhdhr = fhdhr
self.channels = channels
self.fhdhr.db.delete_fhdhr_value("station_scan", "scanning")
def scan(self, waitfordone=False):
self.fhdhr.logger.info("Channel Scan Requested by Client.")
scan_status = self.fhdhr.db.get_fhdhr_value("station_scan", "scanning")
if scan_status:
self.fhdhr.logger.info("Channel Scan Already In Progress!")
else:
self.fhdhr.db.set_fhdhr_value("station_scan", "scanning", 1)
if waitfordone:
self.runscan()
else:
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing"]:
chanscan = multiprocessing.Process(target=self.runscan)
elif self.fhdhr.config.dict["main"]["thread_method"] in ["threading"]:
chanscan = threading.Thread(target=self.runscan)
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
chanscan.start()
def runscan(self):
self.channels.get_channels(forceupdate=True)
self.fhdhr.logger.info("Requested Channel Scan Complete.")
self.fhdhr.db.delete_fhdhr_value("station_scan", "scanning")
def scanning(self):
scan_status = self.fhdhr.db.get_fhdhr_value("station_scan", "scanning")
if not scan_status:
return False
else:
return True

View File

@ -20,31 +20,51 @@ class Tuners():
for i in range(0, self.max_tuners): for i in range(0, self.max_tuners):
self.tuners[str(i)] = Tuner(fhdhr, i, epg) self.tuners[str(i)] = Tuner(fhdhr, i, epg)
def tuner_grab(self, tuner_number): def get_available_tuner(self):
return next(tunernum for tunernum in list(self.tuners.keys()) if not self.tuners[tunernum].tuner_lock.locked()) or None
def get_scanning_tuner(self):
return next(tunernum for tunernum in list(self.tuners.keys()) if self.tuners[tunernum].status["status"] == "Scanning") or None
def stop_tuner_scan(self):
tunernum = self.get_scanning_tuner()
if tunernum:
self.tuners[str(tunernum)].close()
def tuner_scan(self):
"""Temporarily use a tuner for a scan"""
if not self.available_tuner_count():
raise TunerError("805 - All Tuners In Use")
tunernumber = self.get_available_tuner()
self.tuners[str(tunernumber)].channel_scan()
if not tunernumber:
raise TunerError("805 - All Tuners In Use")
def tuner_grab(self, tuner_number, channel_number):
if str(tuner_number) not in list(self.tuners.keys()): if str(tuner_number) not in list(self.tuners.keys()):
self.fhdhr.logger.error("Tuner %s does not exist." % str(tuner_number)) self.fhdhr.logger.error("Tuner %s does not exist." % str(tuner_number))
raise TunerError("806 - Tune Failed") raise TunerError("806 - Tune Failed")
# TunerError will raise if unavailable # TunerError will raise if unavailable
self.tuners[str(tuner_number)].grab() self.tuners[str(tuner_number)].grab(channel_number)
return tuner_number return tuner_number
def first_available(self): def first_available(self, channel_number):
if not self.available_tuner_count(): if not self.available_tuner_count():
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
for tunernum in list(self.tuners.keys()): tunernumber = self.get_available_tuner()
try:
self.tuners[str(tunernum)].grab()
except TunerError:
continue
else:
return tunernum
if not tunernumber:
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
else:
self.tuners[str(tunernumber)].grab(channel_number)
return tunernumber
def tuner_close(self, tunernum): def tuner_close(self, tunernum):
self.tuners[str(tunernum)].close() self.tuners[str(tunernum)].close()
@ -58,16 +78,14 @@ class Tuners():
def available_tuner_count(self): def available_tuner_count(self):
available_tuners = 0 available_tuners = 0
for tunernum in list(self.tuners.keys()): for tunernum in list(self.tuners.keys()):
tuner_status = self.tuners[str(tunernum)].get_status() if not self.tuners[str(tunernum)].tuner_lock.locked():
if tuner_status["status"] == "Inactive":
available_tuners += 1 available_tuners += 1
return available_tuners return available_tuners
def inuse_tuner_count(self): def inuse_tuner_count(self):
inuse_tuners = 0 inuse_tuners = 0
for tunernum in list(self.tuners.keys()): for tunernum in list(self.tuners.keys()):
tuner_status = self.tuners[str(tunernum)].get_status() if self.tuners[str(tunernum)].tuner_lock.locked():
if tuner_status["status"] == "Active":
inuse_tuners += 1 inuse_tuners += 1
return inuse_tuners return inuse_tuners

View File

@ -86,9 +86,6 @@ class Direct_M3U8_Stream():
yield chunk yield chunk
self.tuner.add_downloaded_size(chunk_size) self.tuner.add_downloaded_size(chunk_size)
if playlist.target_duration:
time.sleep(int(playlist.target_duration))
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
except GeneratorExit: except GeneratorExit:

View File

@ -1,3 +1,4 @@
import multiprocessing
import threading import threading
import datetime import datetime
@ -17,17 +18,51 @@ class Tuner():
self.tuner_lock = threading.Lock() self.tuner_lock = threading.Lock()
self.set_off_status() self.set_off_status()
if fhdhr.config.dict["fhdhr"]["address"] == "0.0.0.0":
self.location = ('http://127.0.0.1:%s' % str(fhdhr.config.dict["fhdhr"]["port"]))
else:
self.location = ('http://%s:%s' % (fhdhr.config.dict["fhdhr"]["address"], str(fhdhr.config.dict["fhdhr"]["port"])))
self.chanscan_url = "%s/api/channels?method=scan" % (self.location)
self.close_url = "%s/api/tuners?method=close&tuner=%s" % (self.location, str(self.number))
def channel_scan(self):
if self.tuner_lock.locked():
self.fhdhr.logger.error("Tuner #%s is not available." % str(self.number))
raise TunerError("804 - Tuner In Use")
if self.status["status"] == "Scanning":
self.fhdhr.logger.info("Channel Scan Already In Progress!")
else:
self.tuner_lock.acquire()
self.status["status"] = "Scanning"
self.fhdhr.logger.info("Tuner #%s Performing Channel Scan." % str(self.number))
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing"]:
chanscan = multiprocessing.Process(target=self.runscan)
elif self.fhdhr.config.dict["main"]["thread_method"] in ["threading"]:
chanscan = threading.Thread(target=self.runscan)
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
chanscan.start()
def runscan(self):
self.fhdhr.web.session.get(self.chanscan_url)
self.fhdhr.logger.info("Requested Channel Scan Complete.")
self.fhdhr.web.session.get(self.close_url)
def add_downloaded_size(self, bytes_count): def add_downloaded_size(self, bytes_count):
if "downloaded" in list(self.status.keys()): if "downloaded" in list(self.status.keys()):
self.status["downloaded"] += bytes_count self.status["downloaded"] += bytes_count
def grab(self): def grab(self, channel_number):
if self.tuner_lock.locked(): if self.tuner_lock.locked():
self.fhdhr.logger.error("Tuner #" + str(self.number) + " is not available.") self.fhdhr.logger.error("Tuner #" + str(self.number) + " is not available.")
raise TunerError("804 - Tuner In Use") raise TunerError("804 - Tuner In Use")
self.tuner_lock.acquire() self.tuner_lock.acquire()
self.status["status"] = "Acquired" self.status["status"] = "Acquired"
self.fhdhr.logger.info("Tuner #" + str(self.number) + " Acquired.") self.status["channel"] = channel_number
self.fhdhr.logger.info("Tuner #%s Acquired." % str(self.number))
def close(self): def close(self):
self.set_off_status() self.set_off_status()

View File

@ -3,8 +3,9 @@ from flask import Flask, request
from .pages import fHDHR_Pages from .pages import fHDHR_Pages
from .files import fHDHR_Files from .files import fHDHR_Files
from .hdhr import fHDHR_HDHR
from .rmg import fHDHR_RMG
from .api import fHDHR_API from .api import fHDHR_API
from .watch import fHDHR_WATCH
class fHDHR_HTTP_Server(): class fHDHR_HTTP_Server():
@ -27,14 +28,18 @@ class fHDHR_HTTP_Server():
self.files = fHDHR_Files(fhdhr) self.files = fHDHR_Files(fhdhr)
self.add_endpoints(self.files, "files") self.add_endpoints(self.files, "files")
self.fhdhr.logger.info("Loading HTTP HDHR Endpoints.")
self.hdhr = fHDHR_HDHR(fhdhr)
self.add_endpoints(self.hdhr, "hdhr")
self.fhdhr.logger.info("Loading HTTP RMG Endpoints.")
self.rmg = fHDHR_RMG(fhdhr)
self.add_endpoints(self.rmg, "rmg")
self.fhdhr.logger.info("Loading HTTP API Endpoints.") self.fhdhr.logger.info("Loading HTTP API Endpoints.")
self.api = fHDHR_API(fhdhr) self.api = fHDHR_API(fhdhr)
self.add_endpoints(self.api, "api") self.add_endpoints(self.api, "api")
self.fhdhr.logger.info("Loading HTTP Stream Endpoints.")
self.watch = fHDHR_WATCH(fhdhr)
self.add_endpoints(self.watch, "watch")
self.app.before_request(self.before_request) self.app.before_request(self.before_request)
self.app.after_request(self.after_request) self.app.after_request(self.after_request)

View File

@ -1,12 +1,13 @@
from .root_url import Root_URL
from .cluster import Cluster from .cluster import Cluster
from .settings import Settings from .settings import Settings
from .channels import Channels from .channels import Channels
from .lineup_post import Lineup_Post
from .xmltv import xmlTV from .xmltv import xmlTV
from .m3u import M3U from .m3u import M3U
from .epg import EPG from .epg import EPG
from .watch import Watch from .tuners import Tuners
from .debug import Debug_JSON from .debug import Debug_JSON
from .images import Images from .images import Images
@ -17,14 +18,15 @@ class fHDHR_API():
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.root_url = Root_URL(fhdhr)
self.cluster = Cluster(fhdhr) self.cluster = Cluster(fhdhr)
self.settings = Settings(fhdhr) self.settings = Settings(fhdhr)
self.channels = Channels(fhdhr) self.channels = Channels(fhdhr)
self.xmltv = xmlTV(fhdhr) self.xmltv = xmlTV(fhdhr)
self.m3u = M3U(fhdhr) self.m3u = M3U(fhdhr)
self.epg = EPG(fhdhr) self.epg = EPG(fhdhr)
self.watch = Watch(fhdhr) self.tuners = Tuners(fhdhr)
self.debug = Debug_JSON(fhdhr) self.debug = Debug_JSON(fhdhr)
self.lineup_post = Lineup_Post(fhdhr)
self.images = Images(fhdhr) self.images = Images(fhdhr)

View File

@ -94,7 +94,7 @@ class Channels():
self.fhdhr.device.channels.set_channel_status("id", channel_id, updatedict) self.fhdhr.device.channels.set_channel_status("id", channel_id, updatedict)
elif method == "scan": elif method == "scan":
self.fhdhr.device.station_scan.scan(waitfordone=True) self.fhdhr.device.channels.get_channels(forceupdate=True)
else: else:
return "Invalid Method" return "Invalid Method"

View File

@ -0,0 +1,32 @@
from flask import redirect, request
class Root_URL():
endpoints = ["/"]
endpoint_name = "page_root_html"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
user_agent = request.headers.get('User-Agent')
# Client Devices Discovering Device Information
if not user_agent or str(user_agent).lower().startswith("plexmediaserver"):
# Plex Remote Media Grabber redirect
if self.fhdhr.config.dict["rmg"]["enabled"] and str(user_agent).lower().startswith("plexmediaserver"):
return redirect("/rmg")
# Client Device is looking for HDHR type device
else:
return redirect("/hdhr/device.xml")
# Anything Else is likely a Web Browser
else:
return redirect("/index")

View File

@ -5,10 +5,9 @@ import uuid
from fHDHR.exceptions import TunerError from fHDHR.exceptions import TunerError
class Watch(): class Tuners():
"""Methods to create xmltv.xml""" endpoints = ["/api/tuners"]
endpoints = ["/api/watch"] endpoint_name = "api_tuners"
endpoint_name = "api_watch"
endpoint_methods = ["GET", "POST"] endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr): def __init__(self, fhdhr):
@ -70,9 +69,9 @@ class Watch():
try: try:
if not tuner_number: if not tuner_number:
tunernum = self.fhdhr.device.tuners.first_available() tunernum = self.fhdhr.device.tuners.first_available(channel_number)
else: else:
tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number) tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number, channel_number)
except TunerError as e: except TunerError as e:
self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s"
% (stream_args["method"], str(stream_args["channel"]), str(e))) % (stream_args["method"], str(stream_args["channel"]), str(e)))
@ -109,6 +108,14 @@ class Watch():
tuner = self.fhdhr.device.tuners.tuners[str(tuner_number)] tuner = self.fhdhr.device.tuners.tuners[str(tuner_number)]
tuner.close() tuner.close()
elif method == "scan":
if not tuner_number:
self.fhdhr.device.tuners.tuner_scan()
else:
tuner = self.fhdhr.device.tuners.tuners[str(tuner_number)]
tuner.channel_scan()
else: else:
return "%s Invalid Method" % method return "%s Invalid Method" % method

View File

@ -2,13 +2,7 @@
from .favicon_ico import Favicon_ICO from .favicon_ico import Favicon_ICO
from .style_css import Style_CSS from .style_css import Style_CSS
from .device_xml import Device_XML from .device_xml import Device_XML
from .lineup_xml import Lineup_XML
from .discover_json import Discover_JSON
from .lineup_json import Lineup_JSON
from .lineup_status_json import Lineup_Status_JSON
class fHDHR_Files(): class fHDHR_Files():
@ -18,10 +12,4 @@ class fHDHR_Files():
self.favicon = Favicon_ICO(fhdhr) self.favicon = Favicon_ICO(fhdhr)
self.style = Style_CSS(fhdhr) self.style = Style_CSS(fhdhr)
self.device_xml = Device_XML(fhdhr) self.device_xml = Device_XML(fhdhr)
self.lineup_xml = Lineup_XML(fhdhr)
self.discover_json = Discover_JSON(fhdhr)
self.lineup_json = Lineup_JSON(fhdhr)
self.lineup_status_json = Lineup_Status_JSON(fhdhr)

View File

@ -1,8 +1,4 @@
from flask import Response, request from flask import request, redirect
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class Device_XML(): class Device_XML():
@ -17,31 +13,9 @@ class Device_XML():
def get(self, *args): def get(self, *args):
base_url = request.url_root[:-1] user_agent = request.headers.get('User-Agent')
if (self.fhdhr.config.dict["rmg"]["enabled"] and
out = xml.etree.ElementTree.Element('root') str(user_agent).lower().startswith("plexmediaserver")):
out.set('xmlns', "urn:schemas-upnp-org:device-1-0") return redirect("/rmg/device.xml")
else:
sub_el(out, 'URLBase', base_url) return redirect("/hdhr/device.xml")
specVersion_out = sub_el(out, 'specVersion')
sub_el(specVersion_out, 'major', "1")
sub_el(specVersion_out, 'minor', "0")
device_out = sub_el(out, 'device')
sub_el(device_out, 'deviceType', "urn:schemas-upnp-org:device:MediaServer:1")
sub_el(device_out, 'friendlyName', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'manufacturer', self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"])
sub_el(device_out, 'modelName', self.fhdhr.config.dict["fhdhr"]["reporting_model"])
sub_el(device_out, 'modelNumber', self.fhdhr.config.dict["fhdhr"]["reporting_model"])
sub_el(device_out, 'serialNumber')
sub_el(device_out, 'UDN', "uuid:" + self.fhdhr.config.dict["main"]["uuid"])
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,31 @@
from .lineup_xml import Lineup_XML
from .discover_json import Discover_JSON
from .lineup_json import Lineup_JSON
from .lineup_status_json import Lineup_Status_JSON
from .lineup_post import Lineup_Post
from .device_xml import HDHR_Device_XML
from .auto import Auto
from .tuner import Tuner
class fHDHR_HDHR():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.lineup_post = Lineup_Post(fhdhr)
self.device_xml = HDHR_Device_XML(fhdhr)
self.auto = Auto(fhdhr)
self.tuner = Tuner(fhdhr)
self.lineup_xml = Lineup_XML(fhdhr)
self.discover_json = Discover_JSON(fhdhr)
self.lineup_json = Lineup_JSON(fhdhr)
self.lineup_status_json = Lineup_Status_JSON(fhdhr)

View File

@ -3,8 +3,8 @@ import urllib.parse
class Auto(): class Auto():
endpoints = ['/auto/<channel>'] endpoints = ['/auto/<channel>', '/hdhr/auto/<channel>']
endpoint_name = "watch_auto" endpoint_name = "hdhr_auto"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -16,7 +16,7 @@ class Auto():
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str)
redirect_url = "/api/watch?method=%s" % (method) redirect_url = "/api/tuners?method=%s" % (method)
if channel.startswith("v"): if channel.startswith("v"):
channel_number = channel.replace('v', '') channel_number = channel.replace('v', '')

View File

@ -0,0 +1,53 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class HDHR_Device_XML():
endpoints = ["/hdhr/device.xml"]
endpoint_name = "hdhr_device_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Device.xml referenced from SSDP"""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('root')
out.set('xmlns', "urn:schemas-upnp-org:device-1-0")
sub_el(out, 'URLBase', "%s" % base_url)
specVersion_out = sub_el(out, 'specVersion')
sub_el(specVersion_out, 'major', "1")
sub_el(specVersion_out, 'minor', "0")
device_out = sub_el(out, 'device')
sub_el(device_out, 'deviceType', "urn:schemas-upnp-org:device:MediaServer:1")
sub_el(device_out, 'friendlyName', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'manufacturer', self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"])
sub_el(device_out, 'manufacturerURL', "https://github.com/fHDHR/%s" % self.fhdhr.config.dict["main"]["reponame"])
sub_el(device_out, 'modelName', self.fhdhr.config.dict["fhdhr"]["reporting_model"])
sub_el(device_out, 'modelNumber', self.fhdhr.config.internal["versions"]["fHDHR"])
sub_el(device_out, 'serialNumber')
sub_el(device_out, 'UDN', "uuid:" + self.fhdhr.config.dict["main"]["uuid"])
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -3,8 +3,8 @@ import json
class Discover_JSON(): class Discover_JSON():
endpoints = ["/discover.json"] endpoints = ["/discover.json", "/hdhr/discover.json"]
endpoint_name = "file_discover_json" endpoint_name = "hdhr_discover_json"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -25,8 +25,8 @@ class Discover_JSON():
"FirmwareVersion": self.fhdhr.config.dict["fhdhr"]["reporting_firmware_ver"], "FirmwareVersion": self.fhdhr.config.dict["fhdhr"]["reporting_firmware_ver"],
"DeviceID": self.fhdhr.config.dict["main"]["uuid"], "DeviceID": self.fhdhr.config.dict["main"]["uuid"],
"DeviceAuth": self.fhdhr.config.dict["fhdhr"]["device_auth"], "DeviceAuth": self.fhdhr.config.dict["fhdhr"]["device_auth"],
"BaseURL": base_url, "BaseURL": "%s" % base_url,
"LineupURL": base_url + "/lineup.json" "LineupURL": "%s/lineup.json" % base_url
} }
discover_json = json.dumps(jsondiscover, indent=4) discover_json = json.dumps(jsondiscover, indent=4)

View File

@ -3,8 +3,8 @@ import json
class Lineup_JSON(): class Lineup_JSON():
endpoints = ["/lineup.json"] endpoints = ["/lineup.json", "/hdhr/lineup.json"]
endpoint_name = "file_lineup_json" endpoint_name = "hdhr_lineup_json"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -23,7 +23,7 @@ class Lineup_JSON():
channel_obj = self.fhdhr.device.channels.list[fhdhr_id] channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
if channel_obj.enabled or show == "found": if channel_obj.enabled or show == "found":
lineup_dict = channel_obj.lineup_dict() lineup_dict = channel_obj.lineup_dict()
lineup_dict["URL"] = base_url + lineup_dict["URL"] lineup_dict["URL"] = "%s%s" % (base_url, lineup_dict["URL"])
if show == "found" and channel_obj.enabled: if show == "found" and channel_obj.enabled:
lineup_dict["Enabled"] = 1 lineup_dict["Enabled"] = 1
elif show == "found" and not channel_obj.enabled: elif show == "found" and not channel_obj.enabled:

View File

@ -1,9 +1,11 @@
from flask import request, abort, Response from flask import request, abort, Response
from fHDHR.exceptions import TunerError
class Lineup_Post(): class Lineup_Post():
endpoints = ["/lineup.post"] endpoints = ["/lineup.post", "/hdhr/lineup.post"]
endpoint_name = "api_lineup_post" endpoint_name = "hdhr_lineup_post"
endpoint_methods = ["POST"] endpoint_methods = ["POST"]
def __init__(self, fhdhr): def __init__(self, fhdhr):
@ -17,10 +19,14 @@ class Lineup_Post():
if 'scan' in list(request.args.keys()): if 'scan' in list(request.args.keys()):
if request.args['scan'] == 'start': if request.args['scan'] == 'start':
self.fhdhr.device.station_scan.scan(waitfordone=False) try:
self.fhdhr.device.tuners.tuner_scan()
except TunerError as e:
self.fhdhr.logger.info(str(e))
return Response(status=200, mimetype='text/html') return Response(status=200, mimetype='text/html')
elif request.args['scan'] == 'abort': elif request.args['scan'] == 'abort':
self.fhdhr.device.tuners.stop_tuner_scan()
return Response(status=200, mimetype='text/html') return Response(status=200, mimetype='text/html')
else: else:

View File

@ -3,8 +3,8 @@ import json
class Lineup_Status_JSON(): class Lineup_Status_JSON():
endpoints = ["/lineup_status.json"] endpoints = ["/lineup_status.json", "/hdhr/lineup_status.json"]
endpoint_name = "file_lineup_status_json" endpoint_name = "hdhr_lineup_status_json"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -14,8 +14,13 @@ class Lineup_Status_JSON():
def get(self, *args): def get(self, *args):
station_scanning = self.fhdhr.device.station_scan.scanning() tuner_status = self.fhdhr.device.tuners.status()
if station_scanning: tuners_scanning = 0
for tuner_number in list(tuner_status.keys()):
if tuner_status[tuner_number]["status"] == "Scanning":
tuners_scanning += 1
if tuners_scanning:
jsonlineup = self.scan_in_progress() jsonlineup = self.scan_in_progress()
elif not len(self.fhdhr.device.channels.list): elif not len(self.fhdhr.device.channels.list):
jsonlineup = self.scan_in_progress() jsonlineup = self.scan_in_progress()

View File

@ -6,8 +6,8 @@ from fHDHR.tools import sub_el
class Lineup_XML(): class Lineup_XML():
endpoints = ["/lineup.xml"] endpoints = ["/lineup.xml", "/hdhr/lineup.xml"]
endpoint_name = "file_lineup_xml" endpoint_name = "hdhr_lineup_xml"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr

View File

@ -3,8 +3,8 @@ import urllib.parse
class Tuner(): class Tuner():
endpoints = ['/tuner<tuner_number>/<channel>'] endpoints = ['/tuner<tuner_number>/<channel>', '/hdhr/tuner<tuner_number>/<channel>']
endpoint_name = "watch_tuner" endpoint_name = "hdhr_tuner"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -16,7 +16,7 @@ class Tuner():
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str)
redirect_url = "/api/watch?method=%s" % (method) redirect_url = "/api/tuners?method=%s" % (method)
redirect_url += "&tuner=%s" % str(tuner_number) redirect_url += "&tuner=%s" % str(tuner_number)

View File

@ -5,7 +5,7 @@ from .origin_html import Origin_HTML
from .channels_html import Channels_HTML from .channels_html import Channels_HTML
from .guide_html import Guide_HTML from .guide_html import Guide_HTML
from .cluster_html import Cluster_HTML from .cluster_html import Cluster_HTML
from .streams_html import Streams_HTML from .tuners_html import Tuners_HTML
from .xmltv_html import xmlTV_HTML from .xmltv_html import xmlTV_HTML
from .version_html import Version_HTML from .version_html import Version_HTML
from .diagnostics_html import Diagnostics_HTML from .diagnostics_html import Diagnostics_HTML
@ -24,7 +24,7 @@ class fHDHR_Pages():
self.channels_editor = Channels_Editor_HTML(fhdhr) self.channels_editor = Channels_Editor_HTML(fhdhr)
self.guide_html = Guide_HTML(fhdhr) self.guide_html = Guide_HTML(fhdhr)
self.cluster_html = Cluster_HTML(fhdhr) self.cluster_html = Cluster_HTML(fhdhr)
self.streams_html = Streams_HTML(fhdhr) self.tuners_html = Tuners_HTML(fhdhr)
self.xmltv_html = xmlTV_HTML(fhdhr) self.xmltv_html = xmlTV_HTML(fhdhr)
self.version_html = Version_HTML(fhdhr) self.version_html = Version_HTML(fhdhr)
self.diagnostics_html = Diagnostics_HTML(fhdhr) self.diagnostics_html = Diagnostics_HTML(fhdhr)

View File

@ -13,15 +13,113 @@ class Diagnostics_HTML():
def get(self, *args): def get(self, *args):
# a list of 2 part lists containing button information base_url = request.url_root[:-1]
button_list = [
["debug.json", "/api/debug"], button_list = []
["device.xml", "device.xml"],
["discover.json", "discover.json"], button_list.append({
["lineup.json", "lineup.json"], "label": "Debug Json",
["lineup.xml", "lineup.xml"], "hdhr": None,
["lineup_status.json", "lineup_status.json"], "rmg": None,
["cluster.json", "/api/cluster?method=get"] "other": "/api/debug",
] })
button_list.append({
"label": "Cluster Json",
"hdhr": None,
"rmg": None,
"other": "/api/cluster?method=get",
})
button_list.append({
"label": "Lineup XML",
"hdhr": "/lineup.xml",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Lineup JSON",
"hdhr": "/hdhr/lineup.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Lineup Status",
"hdhr": "/hdhr/lineup_status.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Discover Json",
"hdhr": "/hdhr/discover.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Device XML",
"hdhr": "/hdhr/device.xml",
"rmg": "/rmg/device.xml",
"other": None,
})
button_list.append({
"label": "RMG Identification XML",
"hdhr": "",
"rmg": "/rmg",
"other": None,
})
button_list.append({
"label": "RMG Devices Discover",
"hdhr": "",
"rmg": "/rmg/devices/discover",
"other": None,
})
button_list.append({
"label": "RMG Devices Probe",
"hdhr": "",
"rmg": "/rmg/devices/probe?uri=%s" % base_url,
"other": None,
})
button_list.append({
"label": "RMG Devices by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Channels by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/channels" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Scanners by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/scanners" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Networks by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/networks" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Scan by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/scan" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
return render_template('diagnostics.html', request=request, fhdhr=self.fhdhr, button_list=button_list) return render_template('diagnostics.html', request=request, fhdhr=self.fhdhr, button_list=button_list)

View File

@ -2,8 +2,8 @@ from flask import request, render_template
class Index_HTML(): class Index_HTML():
endpoints = ["/", "/index", "/index.html"] endpoints = ["/index", "/index.html"]
endpoint_name = "page_root_html" endpoint_name = "page_index_html"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr

View File

@ -3,8 +3,8 @@ from flask import request, render_template
from fHDHR.tools import humanized_filesize from fHDHR.tools import humanized_filesize
class Streams_HTML(): class Tuners_HTML():
endpoints = ["/streams", "/streams.html"] endpoints = ["/tuners", "/tuners.html"]
endpoint_name = "page_streams_html" endpoint_name = "page_streams_html"
def __init__(self, fhdhr): def __init__(self, fhdhr):
@ -17,6 +17,7 @@ class Streams_HTML():
tuner_list = [] tuner_list = []
tuner_status = self.fhdhr.device.tuners.status() tuner_status = self.fhdhr.device.tuners.status()
tuner_scanning = 0
for tuner in list(tuner_status.keys()): for tuner in list(tuner_status.keys()):
tuner_dict = { tuner_dict = {
"number": str(tuner), "number": str(tuner),
@ -27,7 +28,9 @@ class Streams_HTML():
tuner_dict["method"] = tuner_status[tuner]["method"] tuner_dict["method"] = tuner_status[tuner]["method"]
tuner_dict["play_duration"] = str(tuner_status[tuner]["Play Time"]) tuner_dict["play_duration"] = str(tuner_status[tuner]["Play Time"])
tuner_dict["downloaded"] = humanized_filesize(tuner_status[tuner]["downloaded"]) tuner_dict["downloaded"] = humanized_filesize(tuner_status[tuner]["downloaded"])
elif tuner_status[tuner]["status"] == "Scanning":
tuner_scanning += 1
tuner_list.append(tuner_dict) tuner_list.append(tuner_dict)
return render_template('streams.html', request=request, fhdhr=self.fhdhr, tuner_list=tuner_list) return render_template('tuners.html', request=request, fhdhr=self.fhdhr, tuner_list=tuner_list, tuner_scanning=tuner_scanning)

View File

@ -0,0 +1,30 @@
from .rmg_ident_xml import RMG_Ident_XML
from .device_xml import RMG_Device_XML
from .devices_discover import RMG_Devices_Discover
from .devices_probe import RMG_Devices_Probe
from .devices_devicekey import RMG_Devices_DeviceKey
from .devices_devicekey_channels import RMG_Devices_DeviceKey_Channels
from .devices_devicekey_scanners import RMG_Devices_DeviceKey_Scanners
from .devices_devicekey_networks import RMG_Devices_DeviceKey_Networks
from .devices_devicekey_scan import RMG_Devices_DeviceKey_Scan
from .devices_devicekey_prefs import RMG_Devices_DeviceKey_Prefs
from .devices_devicekey_media import RMG_Devices_DeviceKey_Media
class fHDHR_RMG():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.rmg_ident_xml = RMG_Ident_XML(fhdhr)
self.device_xml = RMG_Device_XML(fhdhr)
self.devices_discover = RMG_Devices_Discover(fhdhr)
self.devices_probe = RMG_Devices_Probe(fhdhr)
self.devices_devicekey = RMG_Devices_DeviceKey(fhdhr)
self.devices_devicekey_channels = RMG_Devices_DeviceKey_Channels(fhdhr)
self.devices_devicekey_scanners = RMG_Devices_DeviceKey_Scanners(fhdhr)
self.devices_devicekey_networks = RMG_Devices_DeviceKey_Networks(fhdhr)
self.devices_devicekey_scan = RMG_Devices_DeviceKey_Scan(fhdhr)
self.devices_devicekey_prefs = RMG_Devices_DeviceKey_Prefs(fhdhr)
self.devices_devicekey_media = RMG_Devices_DeviceKey_Media(fhdhr)

View File

@ -0,0 +1,58 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Device_XML():
endpoints = ["/rmg/device.xml"]
endpoint_name = "rmg_device_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Device.xml referenced from SSDP"""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('root')
out.set('xmlns', "urn:schemas-upnp-org:device-1-0")
specVersion_out = sub_el(out, 'specVersion')
sub_el(specVersion_out, 'major', "1")
sub_el(specVersion_out, 'minor', "0")
device_out = sub_el(out, 'device')
sub_el(device_out, 'deviceType', "urn:plex-tv:device:Media:1")
sub_el(device_out, 'friendlyName', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'manufacturer', self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"])
sub_el(device_out, 'manufacturerURL', "https://github.com/fHDHR/%s" % self.fhdhr.config.dict["main"]["reponame"])
sub_el(device_out, 'modelName', self.fhdhr.config.dict["fhdhr"]["reporting_model"])
sub_el(device_out, 'modelNumber', self.fhdhr.config.internal["versions"]["fHDHR"])
sub_el(device_out, 'modelDescription', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'modelURL', "https://github.com/fHDHR/%s" % self.fhdhr.config.dict["main"]["reponame"])
serviceList_out = sub_el(device_out, 'serviceList')
service_out = sub_el(serviceList_out, 'service')
sub_el(out, 'URLBase', "%s" % base_url)
sub_el(service_out, 'serviceType', "urn:plex-tv:service:MediaGrabber:1")
sub_el(service_out, 'serviceId', "urn:plex-tv:serviceId:MediaGrabber")
sub_el(device_out, 'UDN', "uuid:%s" % self.fhdhr.config.dict["main"]["uuid"])
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,94 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_DeviceKey():
endpoints = ["/devices/<devicekey>", "/rmg/devices/<devicekey>"]
endpoint_name = "rmg_devices_devicekey"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""Returns the identity, capabilities, and current status of the devices and each of its tuners."""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
out.set('size', "1")
device_out = sub_el(out, 'Device',
key=self.fhdhr.config.dict["main"]["uuid"],
make=self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"],
model=self.fhdhr.config.dict["fhdhr"]["reporting_model"],
modelNumber=self.fhdhr.config.internal["versions"]["fHDHR"],
protocol="livetv",
status="alive",
title=self.fhdhr.config.dict["fhdhr"]["friendlyname"],
tuners=str(self.fhdhr.config.dict["fhdhr"]["tuner_count"]),
uri=base_url,
uuid="device://tv.plex.grabbers.fHDHR/%s" % self.fhdhr.config.dict["main"]["uuid"],
)
tuner_status = self.fhdhr.device.tuners.status()
for tuner_number in list(tuner_status.keys()):
tuner_dict = tuner_status[tuner_number]
# Idle
if tuner_dict["status"] in ["Inactive"]:
sub_el(device_out, 'Tuner',
index=tuner_number,
status="idle",
)
# Streaming
elif tuner_dict["status"] in ["Active", "Acquired"]:
sub_el(device_out, 'Tuner',
index=tuner_number,
status="streaming",
channelIdentifier="id://%s" % tuner_dict["channel"],
signalStrength="100",
signalQuality="100",
symbolQuality="100",
lock="1",
)
# Scanning
elif tuner_dict["status"] in ["Scanning"]:
sub_el(device_out, 'Tuner',
index=tuner_number,
status="scanning",
progress="99",
channelsFound=str(len(self.fhdhr.device.channels.list)),
)
# TODO networksScanned
elif tuner_dict["status"] in ["networksScanned"]:
sub_el(device_out, 'Tuner',
index=tuner_number,
status="networksScanned",
)
# Error
elif tuner_dict["status"] in ["Error"]:
sub_el(device_out, 'Tuner',
index=tuner_number,
status="error",
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,47 @@
from flask import Response
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_DeviceKey_Channels():
endpoints = ["/devices/<devicekey>/channels", "/rmg/devices/<devicekey>/channels"]
endpoint_name = "rmg_devices_devicekey_channels"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""Returns the current channels."""
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
out.set('size', str(len(self.fhdhr.device.channels.list)))
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()):
channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
if channel_obj.enabled:
sub_el(out, 'Channel',
drm="0",
channelIdentifier="id://%s" % channel_obj.dict["number"],
name=channel_obj.dict["name"],
origin=channel_obj.dict["callsign"],
number=str(channel_obj.dict["number"]),
type="tv",
# TODO param
signalStrength="100",
signalQuality="100",
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,31 @@
from flask import request, redirect
import urllib.parse
class RMG_Devices_DeviceKey_Media():
endpoints = ["/devices/<devicekey>/media/<channel>", "/rmg/devices/<devicekey>/media/<channel>"]
endpoint_name = "rmg_devices_devicekey_media"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, channel, *args):
return self.get(devicekey, channel, *args)
def get(self, devicekey, channel, *args):
param = request.args.get('method', default=None, type=str)
self.fhdhr.logger.debug("param:%s" % param)
method = self.fhdhr.config.dict["fhdhr"]["stream_type"]
redirect_url = "/api/tuners?method=%s" % (method)
if str(channel).startswith('id://'):
channel = str(channel).replace('id://', '')
redirect_url += "&channel=%s" % str(channel)
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
return redirect(redirect_url)

View File

@ -0,0 +1,38 @@
from flask import Response
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_DeviceKey_Networks():
endpoints = ["/devices/<devicekey>/networks", "/rmg/devices/<devicekey>/networks"]
endpoint_name = "rmg_devices_devicekey_networks"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""In some cases, channel scanning is a two-step process, where the first stage consists of scanning for networks (this is called "fast scan")."""
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
out.set('size', "1")
sub_el(out, 'Network',
key="1",
title="fHDHR"
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,18 @@
from flask import Response
class RMG_Devices_DeviceKey_Prefs():
endpoints = ["/devices/<devicekey>/prefs", "/rmg/devices/<devicekey>/prefs"]
endpoint_name = "rmg_devices_devicekey_prefs"
endpoint_methods = ["GET", "PUT"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""Prefs sent back from Plex in Key-Pair format"""
return Response(status=200)

View File

@ -0,0 +1,65 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
class RMG_Devices_DeviceKey_Scan():
endpoints = ["/devices/<devicekey>/scan", "/rmg/devices/<devicekey>/scan"]
endpoint_name = "rmg_devices_devicekey_scan"
endpoint_methods = ["GET", "POST", "DELETE"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""Starts a background channel scan."""
if request.method in ["GET", "POST"]:
network = request.args.get('network', default=None, type=str)
source = request.args.get('source', default=None, type=int)
provider = request.args.get('provider', default=1, type=int)
self.fhdhr.logger.debug("Scan Requested network:%s, source:%s, provider:%s" % (network, source, provider))
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
tuner_status = self.fhdhr.device.tuners.status()
tuner_scanning = 0
for tuner in list(tuner_status.keys()):
if tuner_status[tuner]["status"] == "Scanning":
tuner_scanning += 1
if tuner_scanning:
out.set('status', "1")
out.set('message', "Scanning")
else:
out.set('status', "0")
out.set('message', "Not Scanning")
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')
elif request.method in ["DELETE"]:
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
self.fhdhr.device.tuners.stop_tuner_scan()
out.set('status', "0")
out.set('message', "Scan Aborted")
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()

View File

@ -0,0 +1,48 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_DeviceKey_Scanners():
endpoints = ["/devices/<devicekey>/scanners", "/rmg/devices/<devicekey>/scanners"]
endpoint_name = "rmg_devices_devicekey_scanners"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""ascertain which type of scanners are supported."""
method = request.args.get('type', default="0", type=str)
# 0 (atsc), 1 (cqam), 2 (dvb-s), 3 (iptv), 4 (virtual), 5 (dvb-t), 6 (dvb-c), 7 (isdbt)
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
if method == "0":
out.set('size', "1")
out.set('simultaneousScanners', "1")
scanner_out = sub_el(out, 'Scanner',
type="atsc",
# TODO country
)
sub_el(scanner_out, 'Setting',
id="provider",
type="text",
enumValues=self.fhdhr.config.dict["main"]["servicename"]
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,49 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_Discover():
endpoints = ["/devices/discover", "/rmg/devices/discover"]
endpoint_name = "rmg_devices_discover"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""This endpoint requests the grabber attempt to discover any devices it can, and it returns zero or more devices."""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('MediaContainer')
out.set('size', "1")
sub_el(out, 'Device',
key=self.fhdhr.config.dict["main"]["uuid"],
make=self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"],
model=self.fhdhr.config.dict["fhdhr"]["reporting_model"],
modelNumber=self.fhdhr.config.internal["versions"]["fHDHR"],
protocol="livetv",
status="alive",
title=self.fhdhr.config.dict["fhdhr"]["friendlyname"],
tuners=str(self.fhdhr.config.dict["fhdhr"]["tuner_count"]),
uri=base_url,
uuid="device://tv.plex.grabbers.fHDHR/%s" % self.fhdhr.config.dict["main"]["uuid"],
thumb="favicon.ico",
interface='network'
# TODO add preferences
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,51 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_Probe():
endpoints = ["/devices/probe", "/rmg/devices/probe"]
endpoint_name = "rmg_devices_probe"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Probes a specific URI for a network device, and returns a device, if it exists at the given URI."""
base_url = request.url_root[:-1]
uri = request.args.get('uri', default=None, type=str)
out = xml.etree.ElementTree.Element('MediaContainer')
out.set('size', "1")
if uri == base_url:
sub_el(out, 'Device',
key=self.fhdhr.config.dict["main"]["uuid"],
make=self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"],
model=self.fhdhr.config.dict["fhdhr"]["reporting_model"],
modelNumber=self.fhdhr.config.internal["versions"]["fHDHR"],
protocol="livetv",
status="alive",
title=self.fhdhr.config.dict["fhdhr"]["friendlyname"],
tuners=str(self.fhdhr.config.dict["fhdhr"]["tuner_count"]),
uri=base_url,
uuid="device://tv.plex.grabbers.fHDHR/%s" % self.fhdhr.config.dict["main"]["uuid"],
thumb="favicon.ico",
interface='network'
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -0,0 +1,38 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Ident_XML():
endpoints = ["/rmg", "/rmg/"]
endpoint_name = "rmg_ident_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Provides general information about the media grabber"""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('MediaContainer')
sub_el(out, 'MediaGrabber',
identifier="tv.plex.grabbers.fHDHR",
title=str(self.fhdhr.config.dict["fhdhr"]["friendlyname"]),
protocols="livetv",
icon="%s/favicon.ico" % base_url
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -1,12 +0,0 @@
from .auto import Auto
from .tuner import Tuner
class fHDHR_WATCH():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.auto = Auto(fhdhr)
self.tuner = Tuner(fhdhr)

View File

@ -19,8 +19,8 @@ def is_docker():
return False return False
def sub_el(parent, name, text=None, **kwargs): def sub_el(parent, sub_el_item_name, text=None, **kwargs):
el = xml.etree.ElementTree.SubElement(parent, name, **kwargs) el = xml.etree.ElementTree.SubElement(parent, sub_el_item_name, **kwargs)
if text: if text:
el.text = text el.text = text
return el return el