Merge branch 'master' into 'main'

Readding tvhproxy

See merge request homelabers-premium/tvheadend-nm3u8dl!3
This commit is contained in:
2024-03-19 16:41:25 +00:00
11 changed files with 693 additions and 0 deletions

12
tvhProxy/AUTHORS Normal file
View File

@@ -0,0 +1,12 @@
tvhProxy is written and maintained by Joel Kaaberg and
various contributors:
Development Lead
````````````````
- Joel Kaaberg <joel.kaberg@gmail.com>
Patches and Suggestions
```````````````````````
- Nikhil Choudhary

14
tvhProxy/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3-slim
# Sample from https://hub.docker.com/_/python
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip3 install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5004
CMD [ "python3", "./tvhProxy.py" ]

33
tvhProxy/LICENSE Normal file
View File

@@ -0,0 +1,33 @@
Copyright (c) 2016 by Joel Kaaberg and contributors. See AUTHORS
for more details.
Some rights reserved.
Redistribution and use in source and binary forms of the software as well
as documentation, with or without modification, are permitted provided
that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

30
tvhProxy/README.md Normal file
View File

@@ -0,0 +1,30 @@
tvhProxy
========
A small flask app to proxy requests between Plex Media Server and Tvheadend. This repo adds a few critical improvements and fixes to the archived upstream version at [jkaberg/tvhProxy](https://github.com/jkaberg/tvhProxy):
- [SSDP](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) Discovery. Fixes the issue of Plex randomly dropping the device.
- [XMLTV EPG](https://support.plex.tv/articles/using-an-xmltv-guide/) EPG export, including adding dummy programme entries for channels without EPG so you can still use these channels in Plex (see below for Plex configuration URL)
- Configuration of variables via [dotenv](https://pypi.org/project/python-dotenv/) file
#### tvhProxy configuration
1. Check tvhProxy.py for configuration options and set them up them as ```KEY=VALUE``` pairs in a ```.env``` file.
2. Create a virtual enviroment: ```$ python3 -m venv .venv```
3. Activate the virtual enviroment: ```$ . .venv/bin/activate```
4. Install the requirements: ```$ pip install -r requirements.txt```
5. Finally run the app with: ```$ python tvhProxy.py```
#### systemd service configuration
A startup script for Ubuntu can be found in tvhProxy.service (change paths and user in tvhProxy.service to your setup), install with:
$ sudo cp tvhProxy.service /etc/systemd/system/tvhProxy.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable tvhProxy.service
$ sudo systemctl start tvhProxy.service
#### Plex configuration
Enter the IP of the host running tvhProxy including port 5004, eg.: ```192.168.1.50:5004```, use ```http://192.168.1.50:5004/epg.xml``` for the EPG (see [Using XMLTV for guide data](https://support.plex.tv/articles/using-an-xmltv-guide/) for full instructions).
Should the proxy keep disappearing or you can't add it, it's worth trying to delete the existing configuration from the database:
- See https://support.plex.tv/articles/201100678-repair-a-corrupt-database/ on how to access the plex config database and
- run ```delete from media_provider_resources;``` to remove any leftover config.

View File

@@ -0,0 +1,4 @@
flask
requests
gevent
python-dotenv

226
tvhProxy/ssdp.py Normal file
View File

@@ -0,0 +1,226 @@
# 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_PORT = 1900
SSDP_ADDR = '239.255.255.250'
SERVER_ID = 'ZeWaren example SSDP Server'
logger = logging.getLogger()
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', 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 = 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))
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, server=SERVER_ID, 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'] = 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 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 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, SSDP_PORT),
'NTS: ssdp:alive',
]
stcpy = dict(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(map(lambda x: ': '.join(x), stcpy.items()))
resp.extend(('', ''))
logger.debug('do_notify content: %s', resp)
try:
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, SSDP_PORT))
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, 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, SSDP_PORT),
'NTS: ssdp:byebye',
]
try:
stcpy = dict(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(map(lambda x: ': '.join(x), stcpy.items()))
resp.extend(('', ''))
logger.debug('do_byebye content: %s', resp)
if self.sock:
try:
self.sock.sendto('\r\n'.join(resp), (SSDP_ADDR, 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)

View File

@@ -0,0 +1,16 @@
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>{{ data.BaseURL }}</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
<friendlyName>{{ data.FriendlyName }}</friendlyName>
<manufacturer>{{ data.Manufacturer }}</manufacturer>
<modelName>{{ data.ModelNumber }}</modelName>
<modelNumber>{{ data.ModelNumber }}</modelNumber>
<serialNumber></serialNumber>
<UDN>uuid:{{ data.DeviceID }}</UDN>
</device>
</root>

8
tvhProxy/tvhProxy.env Normal file
View File

@@ -0,0 +1,8 @@
TVH_BINDADDR="0.0.0.0"
TVH_BINDPORT=5004
TVH_URL="http://christian:enter@parklife:9981"
TVH_PROXY_URL="http://parklife:5004"
TVH_TUNER_COUNT=6
TVH_WEIGHT=300
TVH_CHUNK_SIZE=1048576
TVH_PROFILE="avlib"

327
tvhProxy/tvhProxy.py Normal file
View File

@@ -0,0 +1,327 @@
from gevent import monkey
monkey.patch_all()
import json
from dotenv import load_dotenv
from ssdp import SSDPServer
from flask import Flask, Response, request, jsonify, abort, render_template
from gevent.pywsgi import WSGIServer
import xml.etree.ElementTree as ElementTree
from datetime import timedelta, datetime, time
import logging
import socket
import threading
from requests.auth import HTTPDigestAuth
import requests
import os
import sched
logging.basicConfig(level=logging.INFO)
load_dotenv(verbose=True)
app = Flask(__name__)
scheduler = sched.scheduler()
logger = logging.getLogger()
host_name = socket.gethostname()
host_ip = socket.gethostbyname(host_name)
# URL format: <protocol>://<username>:<password>@<hostname>:<port>, example: https://test:1234@localhost:9981
config = {
'deviceID': os.environ.get('DEVICE_ID') or '12345678',
'bindAddr': os.environ.get('TVH_BINDADDR') or '',
# only used if set (in case of forward-proxy)
'tvhURL': os.environ.get('TVH_URL') or 'http://localhost:9981',
'tvhProxyURL': os.environ.get('TVH_PROXY_URL'),
'tvhProxyHost': os.environ.get('TVH_PROXY_HOST') or host_ip,
'tvhProxyPort': os.environ.get('TVH_PROXY_PORT') or 5004,
'tvhUser': os.environ.get('TVH_USER') or '',
'tvhPassword': os.environ.get('TVH_PASSWORD') or '',
# number of tuners in tvh
'tunerCount': os.environ.get('TVH_TUNER_COUNT') or 6,
'tvhWeight': os.environ.get('TVH_WEIGHT') or 300, # subscription priority
# usually you don't need to edit this
'chunkSize': os.environ.get('TVH_CHUNK_SIZE') or 1024*1024,
# specifiy a stream profile that you want to use for adhoc transcoding in tvh, e.g. mp4
'streamProfile': os.environ.get('TVH_PROFILE') or 'pass'
}
discoverData = {
'FriendlyName': 'tvhProxy',
'Manufacturer': 'Silicondust',
'ModelNumber': 'HDTC-2US',
'FirmwareName': 'hdhomeruntc_atsc',
'TunerCount': int(config['tunerCount']),
'FirmwareVersion': '20150826',
'DeviceID': config['deviceID'],
'DeviceAuth': 'test1234',
'BaseURL': '%s' % (config['tvhProxyURL'] or "http://" + config['tvhProxyHost'] + ":" + str(config['tvhProxyPort'])),
'LineupURL': '%s/lineup.json' % (config['tvhProxyURL'] or "http://" + config['tvhProxyHost'] + ":" + str(config['tvhProxyPort']))
}
@app.route('/discover.json')
def discover():
return jsonify(discoverData)
@app.route('/lineup_status.json')
def status():
return jsonify({
'ScanInProgress': 0,
'ScanPossible': 0,
'Source': "Cable",
'SourceList': ['Cable']
})
@app.route('/lineup.json')
def lineup():
lineup = []
for c in _get_channels():
if c['enabled']:
url = '%s/stream/channel/%s?profile=%s&weight=%s' % (
config['tvhURL'], c['uuid'], config['streamProfile'], int(config['tvhWeight']))
lineup.append({'GuideNumber': str(c['number']),
'GuideName': c['name'],
'URL': url
})
return jsonify(lineup)
@app.route('/lineup.post', methods=['GET', 'POST'])
def lineup_post():
return ''
@app.route('/')
@app.route('/device.xml')
def device():
return render_template('device.xml', data=discoverData), {'Content-Type': 'application/xml'}
@app.route('/epg.xml')
def epg():
return _get_xmltv(), {'Content-Type': 'application/xml'}
def _get_channels():
url = '%s/api/channel/grid' % config['tvhURL']
params = {
'limit': 999999,
'start': 0
}
try:
r = requests.get(url, params=params, auth=HTTPDigestAuth(
config['tvhUser'], config['tvhPassword']))
return r.json(strict=False)['entries']
except Exception as e:
logger.error('An error occured: %s' + repr(e))
def _get_genres():
def _findMainCategory(majorCategories, minorCategory):
prevKey, currentKey = None, None
for currentKey in sorted(majorCategories.keys()):
if(currentKey > minorCategory):
return majorCategories[prevKey]
prevKey = currentKey
return majorCategories[prevKey]
url = '%s/api/epg/content_type/list' % config['tvhURL']
params = {'full': 1}
try:
r = requests.get(url, auth=HTTPDigestAuth(
config['tvhUser'], config['tvhPassword']))
entries = r.json(strict=False)['entries']
r = requests.get(url, params=params, auth=HTTPDigestAuth(
config['tvhUser'], config['tvhPassword']))
entries_full = r.json(strict=False)['entries']
majorCategories = {}
genres = {}
for entry in entries:
majorCategories[entry['key']] = entry['val']
for entry in entries_full:
if not entry['key'] in majorCategories:
mainCategory = _findMainCategory(majorCategories, entry['key'])
if(mainCategory != entry['val']):
genres[entry['key']] = [mainCategory, entry['val']]
else:
genres[entry['key']] = [entry['val']]
else:
genres[entry['key']] = [entry['val']]
return genres
except Exception as e:
logger.error('An error occured: %s' + repr(e))
def _get_xmltv():
try:
url = '%s/xmltv/channels' % config['tvhURL']
r = requests.get(url, auth=HTTPDigestAuth(
config['tvhUser'], config['tvhPassword']))
logger.info('downloading xmltv from %s', r.url)
tree = ElementTree.ElementTree(
ElementTree.fromstring(requests.get(url, auth=HTTPDigestAuth(config['tvhUser'], config['tvhPassword'])).content))
root = tree.getroot()
url = '%s/api/epg/events/grid' % config['tvhURL']
params = {
'limit': 999999,
'filter': json.dumps([
{
"field": "start",
"type": "numeric",
"value": int(round(datetime.timestamp(datetime.now() + timedelta(hours=72)))),
"comparison": "lt"
}
])
}
r = requests.get(url, params=params, auth=HTTPDigestAuth(
config['tvhUser'], config['tvhPassword']))
logger.info('downloading epg grid from %s', r.url)
epg_events_grid = r.json(strict=False)['entries']
epg_events = {}
event_keys = {}
for epg_event in epg_events_grid:
if epg_event['channelUuid'] not in epg_events:
epg_events[epg_event['channelUuid']] = {}
epg_events[epg_event['channelUuid']
][epg_event['start']] = epg_event
for key in epg_event.keys():
event_keys[key] = True
channelNumberMapping = {}
channelsInEPG = {}
genres = _get_genres()
for child in root:
if child.tag == 'channel':
channelId = child.attrib['id']
channelNo = child[1].text
if not channelNo:
logger.error("No channel number for: %s", channelId)
channelNo = "00"
if not child[0].text:
logger.error("No channel name for: %s", channelNo)
child[0].text = "No Name"
channelNumberMapping[channelId] = channelNo
if channelNo in channelsInEPG:
logger.error("duplicate channelNo: %s", channelNo)
channelsInEPG[channelNo] = False
channelName = ElementTree.Element('display-name')
channelName.text = str(channelNo) + " " + child[0].text
child.insert(0, channelName)
for icon in child.iter('icon'):
# check if icon exists (tvh always returns an URL even if there is no channel icon)
iconUrl = icon.attrib['src']
r = requests.head(iconUrl)
if r.status_code == requests.codes.ok:
icon.attrib['src'] = iconUrl
else:
logger.error("remove icon: %s", iconUrl)
child.remove(icon)
child.attrib['id'] = channelNo
if child.tag == 'programme':
channelUuid = child.attrib['channel']
channelNumber = channelNumberMapping[channelUuid]
channelsInEPG[channelNumber] = True
child.attrib['channel'] = channelNumber
start_datetime = datetime.strptime(
child.attrib['start'], "%Y%m%d%H%M%S %z").astimezone(tz=None).replace(tzinfo=None)
stop_datetime = datetime.strptime(
child.attrib['stop'], "%Y%m%d%H%M%S %z").astimezone(tz=None).replace(tzinfo=None)
if start_datetime >= datetime.now() + timedelta(hours=72):
# Plex doesn't like extremely large XML files, we'll remove the details from entries more than 72h in the future
# Fixed w/ plex server 1.19.2.2673
# for desc in child.iter('desc'):
# child.remove(desc)
pass
elif stop_datetime > datetime.now() and start_datetime < datetime.now() + timedelta(hours=72):
# add extra details for programs in the next 72hs
start_timestamp = int(
round(datetime.timestamp(start_datetime)))
epg_event = epg_events[channelUuid][start_timestamp]
if ('image' in epg_event):
programmeImage = ElementTree.SubElement(child, 'icon')
imageUrl = str(epg_event['image'])
if(imageUrl.startswith('imagecache')):
imageUrl = config['tvhURL'] + \
"/" + imageUrl + ".png"
programmeImage.attrib['src'] = imageUrl
if ('genre' in epg_event):
for genreId in epg_event['genre']:
for category in genres[genreId]:
programmeCategory = ElementTree.SubElement(
child, 'category')
programmeCategory.text = category
if ('episodeOnscreen' in epg_event):
episodeNum = ElementTree.SubElement(
child, 'episode-num')
episodeNum.attrib['system'] = 'onscreen'
episodeNum.text = epg_event['episodeOnscreen']
if('hd' in epg_event):
video = ElementTree.SubElement(child, 'video')
quality = ElementTree.SubElement(video, 'quality')
quality.text = "HDTV"
if('new' in epg_event):
ElementTree.SubElement(child, 'new')
else:
ElementTree.SubElement(child, 'previously-shown')
if('copyright_year' in epg_event):
date = ElementTree.SubElement(child, 'date')
date.text = str(epg_event['copyright_year'])
del epg_events[channelUuid][start_timestamp]
for key in sorted(channelsInEPG):
if channelsInEPG[key]:
logger.debug("Programmes found for channel %s", key)
else:
channelName = root.find(
'channel[@id="'+key+'"]/display-name').text
logger.error("No programme for channel %s: %s",
key, channelName)
# create 2h programmes for 72 hours
yesterday_midnight = datetime.combine(
datetime.today(), time.min) - timedelta(days=1)
date_format = '%Y%m%d%H%M%S'
for x in range(0, 36):
dummyProgramme = ElementTree.SubElement(root, 'programme')
dummyProgramme.attrib['channel'] = str(key)
dummyProgramme.attrib['start'] = (
yesterday_midnight + timedelta(hours=x*2)).strftime(date_format)
dummyProgramme.attrib['stop'] = (
yesterday_midnight + timedelta(hours=(x*2)+2)).strftime(date_format)
dummyTitle = ElementTree.SubElement(
dummyProgramme, 'title')
dummyTitle.attrib['lang'] = 'eng'
dummyTitle.text = channelName
dummyDesc = ElementTree.SubElement(dummyProgramme, 'desc')
dummyDesc.attrib['lang'] = 'eng'
dummyDesc.text = "No programming information"
logger.info("returning epg")
return ElementTree.tostring(root)
except requests.exceptions.RequestException as e: # This is the correct syntax
logger.error('An error occured: ' + repr(e))
raise e
def _start_ssdp():
ssdp = SSDPServer()
thread_ssdp = threading.Thread(target=ssdp.run, args=())
thread_ssdp.daemon = True # Daemonize thread
thread_ssdp.start()
ssdp.register('local',
'uuid:{}::upnp:rootdevice'.format(discoverData['DeviceID']),
'upnp:rootdevice',
'http://{}:{}/device.xml'.format(
config['tvhProxyHost'], config['tvhProxyPort']),
'SSDP Server for tvhProxy')
if __name__ == '__main__':
http = WSGIServer((config['bindAddr'], int(config['tvhProxyPort'])),
app.wsgi_app, log=logger, error_log=logger)
_start_ssdp()
http.serve_forever()

12
tvhProxy/tvhProxy.service Normal file
View File

@@ -0,0 +1,12 @@
[Unit]
Description=A simple proxy for Plex and Tvheadend
After=syslog.target network.target tvheadend.service
[Service]
Environment=
WorkingDirectory=/srv/home/hts/tvhProxy/
ExecStart=/home/hts/tvhProxy/tvhProxy.sh
Restart=always
[Install]
WantedBy=multi-user.target

11
tvhProxy/tvhProxy.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
if [ -f .env ] ; then
source .env
fi
# https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR
source .venv/bin/activate
python tvhProxy.py