]> git.gir.st - subscriptionfeed.git/blob - app/proxy/__init__.py
working HLS proxy (for livestreams)
[subscriptionfeed.git] / app / proxy / __init__.py
1 import re
2 import requests, requests_cache
3 from flask import Flask, Blueprint, request, Response
4 from flask_login import current_user
5
6 frontend = Blueprint('proxy', __name__)
7
8 def app():
9 app = Flask(__name__)
10 app.register_blueprint(frontend)
11 return app
12
13 # https://github.com/psf/requests/issues/5612
14 def silence_errors(generator):
15 try:
16 yield from generator
17 except requests.exceptions.RequestException:
18 return None
19
20 @frontend.route("/videoplayback/<path:path>")
21 @frontend.route("/videoplayback")
22 def videoplayback(path=None):
23 if path is not None: # seg.ts URL
24 args = path.split("/")[0:-1]
25 host = dict(zip(args[::2], args[1::2])).get('hls_chunk_host')
26 path = "/" + path.replace(';','%3B').replace('=','%3D')
27 #host = request.args.get('__host__')
28 else:
29 fvip = request.args.get("fvip", "3")
30 mn = request.args.get("mn", "").split(",")[-1]
31 host = f"r{fvip}---{mn}.googlevideo.com"
32 path = ""
33
34 REQUEST_HEADERS_WHITELIST = {
35 "accept",
36 "accept-encoding",
37 "cache-control",
38 "range",
39 }
40 RESPONSE_HEADERS_WHITELIST = {
41 "accept-ranges",
42 "cache-control",
43 "content-length",
44 "content-range",
45 "content-type",
46 "expires",
47 "x-content-type-options",
48 }
49
50 req_headers = {
51 k:v for k,v in request.headers
52 if k.lower() in REQUEST_HEADERS_WHITELIST
53 }
54 with requests_cache.disabled():
55 r = requests.get(f"https://{host}/videoplayback{path}", request.args.to_dict(), headers=req_headers, stream=True)
56
57 resp_headers = {
58 k:v for k,v in r.headers.items()
59 if k.lower() in RESPONSE_HEADERS_WHITELIST
60 }
61 response = Response(silence_errors(r.iter_content(chunk_size=8192)), status=r.status_code, headers=resp_headers)
62 response.call_on_close(lambda: r.close())
63 response.headers['Access-Control-Allow-Origin'] = '*'
64 return response
65
66 @frontend.route("/api/manifest/hls_playlist/<path:path>")
67 @frontend.route("/api/manifest/hls_variant/<path:path>")
68 def hls_manifest(path):
69 with requests_cache.disabled():
70 path_fixed = request.path.replace(';','%3B').replace('=','%3D')
71 r = requests.get(f"https://manifest.googlevideo.com{path_fixed}", request.args.to_dict())
72 if not r.ok:
73 return "", r.status_code
74
75 lines = []
76 for line in r.text.splitlines():
77 if line.startswith('#'): lines.append(line)
78 elif line.startswith('https://'):
79 scheme, _, host, _path = line.split('/', 3)
80 assert '.googlevideo.com' in host
81 #lines.append("/" + _path + "?__host__=" + host)
82 lines.append("/" + _path )
83 else: raise StopIteration
84 #if line.startswith('https://manifest.googlevideo.com/'):
85 rv = "\n".join(lines)
86 #rv = r.text
87 #rv = rv.replace("https://manifest.googlevideo.com/", f"https://{request.host}/")
88 #rv = re.sub(r"https://([a-z0-9-]+\.googlevideo\.com)", r"https://subscriptions.gir.st/ts-proxy/\1", rv)
89 return rv, {'content-type': 'application/x-mpegURL', 'Access-Control-Allow-Origin': '*'}
90
91
92 @frontend.route("/x-hls/")
93 @frontend.route("/x-hls/<vid>")
94 def xxx(vid="5qap5aO4i9A"):
95 from ..common.common import get_video_info
96 _, map, _, _, _ = get_video_info(vid, -1)
97 url = map['hlsManifestUrl'].replace("https://manifest.googlevideo.com", "")
98 return f"""
99 <!DOCTYPE html>
100 <meta charset="utf-8"/>
101 <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
102 <style>
103 video {{ width: 640px; height: 360px; }}
104 </style>
105 <video autoplay preload="none" controls="true">
106 <source src="{url}" type="application/x-mpegURL"/>
107 </video>
108 <script>
109 let vid = document.querySelector('video');
110 if (!vid.canPlayType('application/vnd.apple.mpegurl') && Hls.isSupported()) {{
111 var hls = new Hls();
112 hls.loadSource(document.querySelector("source").src);
113 hls.attachMedia(vid);
114 }}
115 </script>
116 """
117
118
119 if __name__ == '__main__':
120 app().run(debug=True)
Imprint / Impressum