├── .gitignore ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── liveproxy ├── __init__.py ├── argparser.py ├── main.py └── server.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .coverage.* 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | *.cover 38 | .hypothesis/ 39 | .pytest_cache/ 40 | 41 | # Sphinx documentation 42 | docs/_build/ 43 | 44 | # pyenv 45 | .python-version 46 | 47 | # Environments 48 | .env 49 | .venv 50 | env/ 51 | venv/ 52 | ENV/ 53 | env.bak/ 54 | venv.bak/ 55 | 56 | .vscode/ 57 | 58 | *.m3u 59 | *.m3u8 60 | *.new 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## unreleased 2.1.0.dev4 4 | 5 | ### Added 6 | 7 | - YT-DLP can now also be used. 8 | - new URL with `/cmd/commands without encoding` 9 | 10 | ### Changed 11 | 12 | - only read stdout from stream, ignore stderr 13 | 14 | ### Removed 15 | 16 | - Python 3.6 support 17 | - URL with `/play/` `/streamlink/` `/301/` `/streamlink_301/` 18 | such as `http://127.0.0.1:53422/play/?url=https://foo.bar&q=worst` 19 | - commands to create files for Base64, use direct URLs with /cmd/ instead 20 | 21 | ## 2.0.0 22 | 23 | ### Added 24 | 25 | - Youtube-DL can now also be used. 26 | - New command --loglevel 27 | 28 | ### Changed 29 | 30 | - Streamlink must be used as a binary path, not a Python import. 31 | Example: `streamlink` `streamlink.exe` `/usr/local/bin/streamlink` 32 | - Enigma2 and Kodi requires LiveProxy install on a different local device. 33 | Example: `Raspberry Pi` 34 | 35 | ### Removed 36 | 37 | - Python 2.7 support 38 | - Python 3.5 support 39 | - Streamlink dependency 40 | 41 | ## 1.0.0 42 | 43 | - Fixed bug - base64 TypeError: Incorrect padding 44 | see https://github.com/back-to/liveproxy/commit/3dbcc8550b8aa108a5d84fff2f96b722feb9ab2a 45 | - Fixed Kodi Python3 encoding bug 46 | 47 | ## 0.3.0 48 | 49 | ### Added 50 | 51 | - New command `--file-output` 52 | 53 | ### Changed 54 | 55 | - requiere Streamlink version 1.1.1 56 | - Dropped support for Python 3.4 57 | - Removed streamlink_cli mirror files. 58 | 59 | ## 0.2.0 60 | 61 | ### Changed 62 | 63 | - requiere Streamlink version 1.0.0 64 | - fix if the stream quality name is invalid 65 | 66 | ## 0.1.1 67 | 68 | ### Changed 69 | 70 | - if `--url` was used as an argument, it will be used instead of the nargs 71 | 72 | ## 0.1.0 73 | 74 | ### Added 75 | 76 | - Improve Streamlink default Plugins load speed 77 | - New commands `--file` and `--format` got added, 78 | they can create valid URLs from a file for the new base64 URL style. 79 | 80 | ### Changed 81 | 82 | - Custom plugins with `from streamlink.plugin.api import http` are not allowed, 83 | use `self.session` 84 | - The LiveProxy URL build was simplified, a Streamlink command like 85 | `streamlink https://www.youtube.com/user/france24 best` 86 | can be used after it got base64 encoded. 87 | URL example `http://127.0.0.1:53422/base64/c3RyZWFtbGluayBodHRwczovL3d3dy55b3V0dWJlLmNvbS91c2VyL2ZyYW5jZTI0IGJlc3Q=/` 88 | more details can be found on the website. 89 | 90 | ## 0.0.3 91 | 92 | Skip 0.0.2 because it was used as a hotfix for E2. 93 | 94 | ### Added 95 | 96 | - Allow FFmpeg and RTMP streams, they might not work on every platform, 97 | they work on Linux, but not on Windows for me. 98 | 99 | Don't use them if it doesn't work for you, 100 | this streamlink command can be used to disable them. 101 | https://streamlink.github.io/cli.html#cmdoption-stream-types 102 | 103 | ### Changed 104 | 105 | - Streamlink version 0.14.0 is required 106 | - Removed help text from mirror argparser 107 | - Allow `/streamlink/` for `/path/` in URLs 108 | - Allow `/streamlink_301/` for `/301/` in URLs 109 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | 4 | prune */__pycache__ 5 | global-exclude *.pyc *~ *.bak *.swp *.pyo 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveProxy 2 | 3 | LiveProxy can redirect Livestreams to your favorite player on a lot of devices. 4 | 5 | - Issue Tracker: https://github.com/back-to/liveproxy/issues 6 | - Github: https://github.com/back-to/liveproxy 7 | 8 | # INSTALLATION 9 | 10 | ## pip as user 11 | 12 | ```sh 13 | # Latest pip version: 14 | python3 -m pip install --upgrade liveproxy 15 | 16 | # Latest dev version: 17 | python3 -m pip install --upgrade git+https://github.com/back-to/liveproxy.git 18 | ``` 19 | 20 | ## pip as root 21 | 22 | ```sh 23 | # Latest pip version: 24 | sudo -H python3 -m pip install --upgrade liveproxy 25 | 26 | # Latest dev version: 27 | sudo -H python3 -m pip install --upgrade git+https://github.com/back-to/liveproxy.git 28 | ``` 29 | 30 | # URL-GUIDE 31 | 32 | ## Tutorial 33 | 34 | First, start LiveProxy on your system. 35 | 36 | ```text 37 | $ liveproxy 38 | [main][INFO] For LiveProxy support visit https://github.com/back-to/liveproxy 39 | [main][INFO] Starting server: 127.0.0.1 on port 53422 40 | ``` 41 | 42 | host and port can be changed with `--host` / `--port` 43 | 44 | ```text 45 | $ liveproxy --host 0.0.0.0 --port 12345 46 | [main][INFO] For LiveProxy support visit https://github.com/back-to/liveproxy 47 | [main][INFO] Starting server: 0.0.0.0 on port 12345 48 | ``` 49 | 50 | Now that LiveProxy is running, you will have to create a valid URL. 51 | 52 | For the examples here, ``53422`` is used as **default port**. 53 | 54 | ## CMD 55 | 56 | If needed, you can use `quote` for special characters 57 | https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote 58 | 59 | ```text 60 | http://127.0.0.1:53422/cmd/COMMANDS/ 61 | ``` 62 | 63 | Example for `streamlink https://www.youtube.com/user/france24/live best` 64 | 65 | ```text 66 | http://127.0.0.1:53422/cmd/streamlink https://www.youtube.com/user/france24/live best/ 67 | ``` 68 | 69 | Example for `yt-dlp https://www.youtube.com/user/france24/live` 70 | 71 | ```text 72 | http://127.0.0.1:53422/cmd/yt-dlp https://www.youtube.com/user/france24/live/ 73 | ``` 74 | 75 | ## Base64 76 | 77 | You will need to base64 encode your used commands. 78 | 79 | #### Streamlink 80 | 81 | ```text 82 | http://127.0.0.1:53422/base64/STREAMLINK-COMMANDS/ 83 | ``` 84 | 85 | Example for `streamlink https://www.youtube.com/user/france24/live best` 86 | 87 | ```text 88 | http://127.0.0.1:53422/base64/c3RyZWFtbGluayBodHRwczovL3d3dy55b3V0dWJlLmNvbS91c2VyL2ZyYW5jZTI0L2xpdmUgYmVzdA==/ 89 | ``` 90 | 91 | #### Youtube-DL 92 | 93 | ```text 94 | http://127.0.0.1:53422/base64/YOUTUBE-DL-COMMANDS/ 95 | ``` 96 | 97 | Example for `youtube-dl https://www.youtube.com/user/france24/live` 98 | 99 | ```text 100 | http://127.0.0.1:53422/base64/eW91dHViZS1kbCBodHRwczovL3d3dy55b3V0dWJlLmNvbS91c2VyL2ZyYW5jZTI0L2xpdmU=/ 101 | ``` 102 | 103 | #### YT-DLP 104 | 105 | ```text 106 | http://127.0.0.1:53422/base64/YT-DLP-COMMANDS/ 107 | ``` 108 | 109 | Example for `yt-dlp https://www.youtube.com/user/france24/live` 110 | 111 | ```text 112 | http://127.0.0.1:53422/base64/eXQtZGxwIGh0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3VzZXIvZnJhbmNlMjQvbGl2ZQ==/ 113 | ``` 114 | -------------------------------------------------------------------------------- /liveproxy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0.dev4" 2 | -------------------------------------------------------------------------------- /liveproxy/argparser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from textwrap import dedent 3 | 4 | from liveproxy import __version__ as liveproxy_version 5 | 6 | 7 | def num(type, min=None, max=None): 8 | def func(value): 9 | value = type(value) 10 | if min is not None and not (value > min): 11 | raise argparse.ArgumentTypeError( 12 | "{0} value must be more than {1} but is {2}".format( 13 | type.__name__, min, value 14 | ) 15 | ) 16 | if max is not None and not (value <= max): 17 | raise argparse.ArgumentTypeError( 18 | "{0} value must be at most {1} but is {2}".format( 19 | type.__name__, max, value 20 | ) 21 | ) 22 | return value 23 | 24 | func.__name__ = type.__name__ 25 | return func 26 | 27 | 28 | parser = argparse.ArgumentParser( 29 | fromfile_prefix_chars="@", 30 | add_help=False, 31 | usage="%(prog)s --host [HOST] --port [PORT]", 32 | description=dedent(""" 33 | LiveProxy is a local URL Proxy for Streamlink 34 | """), 35 | epilog=dedent(""" 36 | For more in-depth documentation see: 37 | https://github.com/back-to/liveproxy/blob/master/README.md 38 | """) 39 | ) 40 | 41 | general = parser.add_argument_group("General options") 42 | general.add_argument( 43 | "-h", "--help", 44 | action="store_true", 45 | help=""" 46 | Show this help message and exit. 47 | """ 48 | ) 49 | general.add_argument( 50 | "-V", "--version", 51 | action="version", 52 | version="%(prog)s {0}".format(liveproxy_version), 53 | help=""" 54 | Show version number and exit. 55 | """ 56 | ) 57 | general.add_argument( 58 | "--loglevel", 59 | metavar="LEVEL", 60 | choices=("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"), 61 | default="INFO", 62 | help=""" 63 | Set the log message threshold. 64 | 65 | https://docs.python.org/3/library/logging.html#logging-levels 66 | 67 | Valid levels are: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET 68 | """ 69 | ) 70 | 71 | server = parser.add_argument_group("Server options") 72 | server.add_argument( 73 | "--host", 74 | metavar="HOST", 75 | type=str, 76 | default="127.0.0.1", 77 | help=""" 78 | A fixed IP to use as a HOST. 79 | 80 | Default is 127.0.0.1 81 | """ 82 | ) 83 | server.add_argument( 84 | "--port", 85 | metavar="PORT", 86 | type=num(int, min=0, max=65535), 87 | default=53422, 88 | help=""" 89 | A fixed PORT to use for the HOST. 90 | 91 | Default is 53422 92 | """ 93 | ) 94 | 95 | __all__ = ["parser"] 96 | -------------------------------------------------------------------------------- /liveproxy/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import errno 3 | import logging 4 | import os 5 | import platform 6 | import sys 7 | 8 | from liveproxy import __version__ as liveproxy_version 9 | from liveproxy.argparser import parser 10 | from liveproxy.server import HTTPRequest, ThreadedHTTPServer 11 | 12 | log = logging.getLogger(__name__.replace("liveproxy.", "")) 13 | 14 | 15 | def main(): 16 | error_code = 0 17 | args = parser.parse_args(sys.argv[1:]) 18 | if args.help: 19 | parser.print_help() 20 | return 21 | 22 | logging.basicConfig( 23 | stream=sys.stdout, 24 | level=args.loglevel, 25 | format="[%(name)s][%(levelname)s] %(message)s", 26 | ) 27 | 28 | if hasattr(os, "getuid"): 29 | if os.geteuid() == 0: 30 | log.info("LiveProxy is running as root! Be careful!") 31 | 32 | # MAC OS X 33 | if sys.platform == "darwin": 34 | os_version = f"macOS {platform.mac_ver()[0]}" 35 | # Windows 36 | elif sys.platform.startswith("win"): 37 | os_version = f"{platform.system()} {platform.release()}" 38 | # Linux / other 39 | else: 40 | os_version = platform.platform() 41 | 42 | log.info("For LiveProxy support visit https://github.com/back-to/liveproxy") 43 | log.debug(f"OS: {os_version}") 44 | log.debug(f"Python: {platform.python_version()}") 45 | log.debug(f"LiveProxy: {liveproxy_version}") 46 | 47 | HOST = str(args.host) 48 | PORT = int(args.port) 49 | 50 | log.info(f"Starting server: {HOST} on port {PORT}") 51 | 52 | try: 53 | httpd = ThreadedHTTPServer((HOST, PORT), HTTPRequest) 54 | except OSError as err: 55 | if err.errno == errno.EADDRINUSE: 56 | log.error(f"Could not listen on port {PORT}! Exiting...") 57 | sys.exit(errno.EADDRINUSE) 58 | elif err.errno == errno.EADDRNOTAVAIL: 59 | log.error(f"Cannot assign requested address {HOST}") 60 | sys.exit(err.errno) 61 | log.error(f"Error {err}! Exiting...") 62 | sys.exit(err.errno) 63 | try: 64 | httpd.serve_forever() 65 | except KeyboardInterrupt: 66 | # close server 67 | if httpd: 68 | httpd.shutdown() 69 | httpd.server_close() 70 | log.error("Interrupted! Exiting...") 71 | error_code = 130 72 | finally: 73 | if httpd: 74 | try: 75 | log.info(f"Closing server {HOST} on port {PORT} ...") 76 | httpd.shutdown() 77 | httpd.server_close() 78 | except KeyboardInterrupt: 79 | error_code = 130 80 | 81 | sys.exit(error_code) 82 | -------------------------------------------------------------------------------- /liveproxy/server.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import errno 3 | import logging 4 | import os 5 | import re 6 | import shlex 7 | import socket 8 | import subprocess 9 | import sys 10 | from http.server import BaseHTTPRequestHandler, HTTPServer 11 | from shutil import which 12 | from socketserver import ThreadingMixIn 13 | from time import time 14 | from urllib.parse import unquote 15 | 16 | ACCEPTABLE_ERRNO = ( 17 | errno.ECONNABORTED, 18 | errno.ECONNRESET, 19 | errno.EINVAL, 20 | errno.EPIPE, 21 | ) 22 | try: 23 | ACCEPTABLE_ERRNO += (errno.WSAECONNABORTED,) 24 | except AttributeError: 25 | pass # Not windows 26 | 27 | _re_streamlink = re.compile(r"streamlink", re.IGNORECASE) 28 | _re_youtube_dl = re.compile(r"(?:youtube|yt)[_-]dl(?:p)?", re.IGNORECASE) 29 | 30 | log = logging.getLogger(__name__.replace("liveproxy.", "")) 31 | 32 | 33 | class HTTPRequest(BaseHTTPRequestHandler): 34 | 35 | def log_message(self, format, *args): 36 | pass 37 | 38 | def _headers(self, status, content, connection=False): 39 | self.send_response(status) 40 | self.send_header("Server", "LiveProxy") 41 | self.send_header("Content-type", content) 42 | if connection: 43 | self.send_header("Connection", connection) 44 | self.end_headers() 45 | 46 | def do_HEAD(self): 47 | """Respond to a HEAD request.""" 48 | self._headers(404, "text/html", connection="close") 49 | 50 | def do_GET(self): 51 | """Respond to a GET request.""" 52 | random_id = hex(int(time()))[5:] 53 | log = logging.getLogger("{name}.{random_id}".format( 54 | name=__name__.replace("liveproxy.", ""), 55 | random_id=random_id, 56 | )) 57 | 58 | log.info(f"User-Agent: {self.headers.get('User-Agent', '???')}") 59 | log.info(f"Client: {self.client_address}") 60 | log.info(f"Address: {self.address_string()}") 61 | 62 | if self.path.startswith(("/base64/")): 63 | # http://127.0.0.1:53422/base64/STREAMLINK-COMMANDS/ 64 | # http://127.0.0.1:53422/base64/YOUTUBE-DL-COMMANDS/ 65 | # http://127.0.0.1:53422/base64/YT-DLP-COMMANDS/ 66 | try: 67 | arglist = shlex.split(base64.urlsafe_b64decode(self.path.split("/")[2]).decode("UTF-8")) 68 | except base64.binascii.Error as err: 69 | log.error(f"invalid base64 URL: {err}") 70 | self._headers(404, "text/html", connection="close") 71 | return 72 | elif self.path.startswith(("/cmd/")): 73 | # http://127.0.0.1:53422/cmd/streamlink https://example best/ 74 | self.path = self.path[5:] 75 | if self.path.endswith("/"): 76 | self.path = self.path[:-1] 77 | arglist = shlex.split(unquote(self.path)) 78 | else: 79 | self._headers(404, "text/html", connection="close") 80 | return 81 | 82 | prog = which(arglist[0], mode=os.F_OK | os.X_OK) 83 | if not prog: 84 | log.error(f"invalid prog, can not find '{arglist[0]}' on your system") 85 | return 86 | 87 | log.debug(f"Video-Software: {prog}") 88 | if _re_streamlink.search(prog): 89 | arglist.extend(["--stdout", "--loglevel", "none"]) 90 | elif _re_youtube_dl.search(prog): 91 | arglist.extend(["-o", "-", "--quiet", "--no-playlist", "--no-warnings", "--no-progress"]) 92 | else: 93 | log.error("Video-Software is not supported.") 94 | self._headers(404, "text/html", connection="close") 95 | return 96 | 97 | log.debug(f"{arglist!r}") 98 | self._headers(200, "video/unknown") 99 | process = subprocess.Popen(arglist, 100 | stderr=subprocess.PIPE, 101 | stdin=subprocess.PIPE, 102 | stdout=subprocess.PIPE, 103 | shell=False, 104 | ) 105 | 106 | log.info(f"Stream started {random_id}") 107 | try: 108 | while True: 109 | read = process.stdout.readline() 110 | if read: 111 | self.wfile.write(read) 112 | sys.stdout.flush() 113 | if process.poll() is not None: 114 | self.wfile.close() 115 | break 116 | except socket.error as e: 117 | if isinstance(e.args, tuple): 118 | if not e.errno in ACCEPTABLE_ERRNO: 119 | log.error(f"E1: {e!r}") 120 | else: 121 | log.error(f"E2: {e!r}") 122 | 123 | log.info(f"Stream ended {random_id}") 124 | process.terminate() 125 | process.wait() 126 | process.kill() 127 | 128 | 129 | class Server(HTTPServer): 130 | """HTTPServer class with timeout.""" 131 | timeout = 5 132 | 133 | def finish_request(self, request, client_address): 134 | """Finish one request by instantiating RequestHandlerClass.""" 135 | try: 136 | self.RequestHandlerClass(request, client_address, self) 137 | except ValueError: 138 | pass 139 | except socket.error as err: 140 | if err.errno not in ACCEPTABLE_ERRNO: 141 | raise 142 | 143 | 144 | class ThreadedHTTPServer(ThreadingMixIn, Server): 145 | """Handle requests in a separate thread.""" 146 | allow_reuse_address = True 147 | daemon_threads = True 148 | 149 | 150 | __all__ = ("HTTPRequest", "ThreadedHTTPServer") 151 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # W503 - line break before binary operator 4 | W503, 5 | exclude = 6 | __pycache__, 7 | .git, 8 | build, 9 | dist, 10 | env, 11 | old, 12 | venv, 13 | max-line-length = 128 14 | show-source = True 15 | statistic = True 16 | import-order-style = pycharm 17 | application-import-names = 18 | liveproxy, 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import codecs 3 | import os 4 | import re 5 | 6 | from setuptools import setup 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | def read(*parts): 12 | with codecs.open(os.path.join(here, *parts), 'r') as fp: 13 | return fp.read() 14 | 15 | 16 | def find_version(*file_paths): 17 | version_file = read(*file_paths) 18 | version_match = re.search( 19 | r'''^__version__ = ['"]([^'"]*)['"]''', 20 | version_file, 21 | re.M, 22 | ) 23 | if version_match: 24 | return version_match.group(1) 25 | 26 | raise RuntimeError('Unable to find version string.') 27 | 28 | 29 | long_description = read('README.md') 30 | 31 | setup( 32 | name='liveproxy', 33 | version=find_version('liveproxy', '__init__.py'), 34 | description='LiveProxy can redirect Livestreams to your favorite player', 35 | long_description=long_description, 36 | long_description_content_type='text/markdown', 37 | url='https://github.com/back-to/liveproxy', 38 | project_urls={ 39 | 'Source': 'https://github.com/back-to/liveproxy/', 40 | 'Tracker': 'https://github.com/back-to/liveproxy/issues', 41 | }, 42 | author='back-to', 43 | author_email='backto@protonmail.ch', 44 | packages=['liveproxy'], 45 | entry_points={'console_scripts': ['liveproxy=liveproxy.main:main']}, 46 | python_requires='>=3.7, <4', 47 | classifiers=[ 48 | 'Development Status :: 4 - Beta', 49 | 'Environment :: Console', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python', 52 | 'Topic :: Internet :: WWW/HTTP', 53 | 'Topic :: Multimedia :: Video', 54 | ], 55 | keywords='LiveProxy Streamlink Youtube-DL YT-DLP', 56 | ) 57 | --------------------------------------------------------------------------------