├── requirements.txt ├── AUTHORS ├── tvhProxy.service ├── templates └── device.xml ├── README.md ├── LICENSE └── tvhProxy.py /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | requests 3 | gevent -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | tvhProxy is written and maintained by Joel Kaaberg and 2 | various contributors: 3 | 4 | Development Lead 5 | ```````````````` 6 | 7 | - Joel Kaaberg 8 | 9 | Patches and Suggestions 10 | ``````````````````````` 11 | 12 | - Nikhil Choudhary -------------------------------------------------------------------------------- /tvhProxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=A simple proxy for Plex and Tvheadend 3 | 4 | [Service] 5 | Environment= 6 | WorkingDirectory=/home/tvh/tvhProxy/ 7 | ExecStart=/home/tvh/tvhProxy/venv/bin/python /home/tvh/tvhProxy/tvhProxy.py 8 | Restart=always 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /templates/device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 4 | 0 5 | 6 | {{ data.BaseURL }} 7 | 8 | urn:schemas-upnp-org:device:MediaServer:1 9 | {{ data.FriendlyName }} 10 | {{ data.Manufacturer }} 11 | {{ data.ModelNumber }} 12 | {{ data.ModelNumber }} 13 | 14 | uuid:{{ data.DeviceID }} 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tvhProxy 2 | ======== 3 | 4 | A small flask app to proxy requests between Plex Media Server and Tvheadend. 5 | 6 | #### tvhProxy configuration 7 | 1. In tvhProxy.py configure options as per your setup. 8 | 2. Create a virtual enviroment: ```$ virtualenv venv``` 9 | 3. Activate the virtual enviroment: ```$ . venv/bin/activate``` 10 | 4. Install the requirements: ```$ pip install -r requirements.txt``` 11 | 5. Finally run the app with: ```$ python tvhProxy.py``` 12 | 13 | #### systemd service configuration 14 | A startup script for Ubuntu can be found in tvhProxy.service (change paths in tvhProxy.service to your setup), install with: 15 | 16 | $ sudo cp tvhProxy.service /etc/systemd/system/tvhProxy.service 17 | $ sudo systemctl daemon-reload 18 | $ sudo systemctl enable tvhProxy.service 19 | $ sudo systemctl start tvhProxy.service 20 | 21 | #### Plex configuration 22 | Enter the IP of the host running tvhProxy including port 5004, eg.: ```192.168.1.50:5004``` 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by Joel Kaaberg and contributors. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. -------------------------------------------------------------------------------- /tvhProxy.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | 3 | import time 4 | import os 5 | import requests 6 | from gevent.pywsgi import WSGIServer 7 | from flask import Flask, Response, request, jsonify, abort, render_template 8 | 9 | app = Flask(__name__) 10 | 11 | # URL format: ://:@:, example: https://test:1234@localhost:9981 12 | config = { 13 | 'bindAddr': os.environ.get('TVH_BINDADDR') or '', 14 | 'tvhURL': os.environ.get('TVH_URL') or 'http://test:test@localhost:9981', 15 | 'tvhProxyURL': os.environ.get('TVH_PROXY_URL') or 'http://localhost', 16 | 'tunerCount': os.environ.get('TVH_TUNER_COUNT') or 6, # number of tuners in tvh 17 | 'tvhWeight': os.environ.get('TVH_WEIGHT') or 300, # subscription priority 18 | 'chunkSize': os.environ.get('TVH_CHUNK_SIZE') or 1024*1024, # usually you don't need to edit this 19 | 'streamProfile': os.environ.get('TVH_PROFILE') or 'pass' # specifiy a stream profile that you want to use for adhoc transcoding in tvh, e.g. mp4 20 | } 21 | 22 | discoverData = { 23 | 'FriendlyName': 'tvhProxy', 24 | 'Manufacturer' : 'Silicondust', 25 | 'ModelNumber': 'HDTC-2US', 26 | 'FirmwareName': 'hdhomeruntc_atsc', 27 | 'TunerCount': int(config['tunerCount']), 28 | 'FirmwareVersion': '20150826', 29 | 'DeviceID': '12345678', 30 | 'DeviceAuth': 'test1234', 31 | 'BaseURL': '%s' % config['tvhProxyURL'], 32 | 'LineupURL': '%s/lineup.json' % config['tvhProxyURL'] 33 | } 34 | 35 | @app.route('/discover.json') 36 | def discover(): 37 | return jsonify(discoverData) 38 | 39 | 40 | @app.route('/lineup_status.json') 41 | def status(): 42 | return jsonify({ 43 | 'ScanInProgress': 0, 44 | 'ScanPossible': 1, 45 | 'Source': "Cable", 46 | 'SourceList': ['Cable'] 47 | }) 48 | 49 | 50 | @app.route('/lineup.json') 51 | def lineup(): 52 | lineup = [] 53 | 54 | for c in _get_channels(): 55 | if c['enabled']: 56 | url = '%s/stream/channel/%s?profile=%s&weight=%s' % (config['tvhURL'], c['uuid'], config['streamProfile'],int(config['tvhWeight'])) 57 | 58 | lineup.append({'GuideNumber': str(c['number']), 59 | 'GuideName': c['name'], 60 | 'URL': url 61 | }) 62 | 63 | return jsonify(lineup) 64 | 65 | 66 | @app.route('/lineup.post', methods=['GET', 'POST']) 67 | def lineup_post(): 68 | return '' 69 | 70 | @app.route('/') 71 | @app.route('/device.xml') 72 | def device(): 73 | return render_template('device.xml',data = discoverData),{'Content-Type': 'application/xml'} 74 | 75 | 76 | def _get_channels(): 77 | url = '%s/api/channel/grid?start=0&limit=999999' % config['tvhURL'] 78 | 79 | try: 80 | r = requests.get(url) 81 | return r.json()['entries'] 82 | 83 | except Exception as e: 84 | print('An error occured: ' + repr(e)) 85 | 86 | 87 | if __name__ == '__main__': 88 | http = WSGIServer((config['bindAddr'], 5004), app.wsgi_app) 89 | http.serve_forever() 90 | --------------------------------------------------------------------------------