diff --git a/backend/config.py b/backend/config.py index 5b5092f..d319e42 100644 --- a/backend/config.py +++ b/backend/config.py @@ -155,7 +155,9 @@ for key in providers: proxies[elem] = [] new_providers[elem] = providers[key] - for proxy_link, region_name, country in zip(proxy_current, region_current, proxy_countries): + for proxy_link, region_name, country in zip( + proxy_current, region_current, proxy_countries + ): new_key = key if country is not None: new_key = f"{key}_{country}" @@ -177,8 +179,12 @@ for proxy_provider in proxies.values(): if isinstance(proxy_elem, ProxyElem) and isinstance(proxy_elem.proxy, str): proxy_keys.append(proxy_elem.proxy) streamlink_sessions[proxy_elem.proxy] = streamlink.Streamlink() - streamlink_sessions[proxy_elem.proxy].set_option("http-proxy", "socks5://" + proxy_elem.proxy) - streamlink_sessions[proxy_elem.proxy].set_option("https-proxy", "socks5://" + proxy_elem.proxy) + streamlink_sessions[proxy_elem.proxy].set_option( + "http-proxy", "socks5://" + proxy_elem.proxy + ) + streamlink_sessions[proxy_elem.proxy].set_option( + "https-proxy", "socks5://" + proxy_elem.proxy + ) if icecast_server is not None and stream_server is not None: try: diff --git a/backend/sources.py b/backend/sources.py index 278ef53..bb93207 100755 --- a/backend/sources.py +++ b/backend/sources.py @@ -6,7 +6,9 @@ import json playlist = None try: - resp = requests.get("https://git.purser.it/roypur/icecast-relay/raw/branch/master/icecast.xml") + resp = requests.get( + "https://git.purser.it/roypur/icecast-relay/raw/branch/master/icecast.xml" + ) dom = xml.dom.minidom.parseString(resp.text) except Exception as e: print(e) @@ -39,7 +41,9 @@ except Exception as e: print(e) else: try: - resp = requests.get(f'https://cdn.jsdelivr.net/npm/@silvermine/videojs-chromecast@{chromecast_version}/dist/silvermine-videojs-chromecast.min.css') + resp = requests.get( + f"https://cdn.jsdelivr.net/npm/@silvermine/videojs-chromecast@{chromecast_version}/dist/silvermine-videojs-chromecast.min.css" + ) except Exception as e: print(e) else: @@ -48,17 +52,19 @@ else: with open("/app/version/chromecast.txt", "w") as f: f.write(chromecast_version) + def store_cdnjs(name): version = None try: - resp = requests.get(f'https://api.cdnjs.com/libraries/{name}?fields=version') + resp = requests.get(f"https://api.cdnjs.com/libraries/{name}?fields=version") data = json.loads(resp.text) version = data["version"] except Exception as e: print(e) else: - with open(f'/app/version/{name}.txt', "w") as f: + with open(f"/app/version/{name}.txt", "w") as f: f.write(version) + store_cdnjs("video.js") store_cdnjs("font-awesome") diff --git a/backend/stream.py b/backend/stream.py index b706431..c387b96 100755 --- a/backend/stream.py +++ b/backend/stream.py @@ -10,11 +10,15 @@ import config import stream_providers from typing import Optional, cast, Any -logging.basicConfig(format='[%(filename)s:%(lineno)d] %(message)s', stream=sys.stdout, level=logging.INFO) +logging.basicConfig( + format="[%(filename)s:%(lineno)d] %(message)s", + stream=sys.stdout, + level=logging.INFO, +) logger = logging.getLogger(__name__) -class UpstreamHandler(): +class UpstreamHandler: def __init__(self): self.provider: Optional[str] = None self.raw: bool = False @@ -45,18 +49,18 @@ class UpstreamHandler(): raw_str = handler.get_query_argument("raw", None) direct_str = handler.get_query_argument("direct", None) - true_values = ['y', 'yes', 't', 'true', 'on', '1'] + true_values = ["y", "yes", "t", "true", "on", "1"] if isinstance(direct_str, str): try: - self.direct = (direct_str.lower() in true_values) + self.direct = direct_str.lower() in true_values except ValueError as e: logger.info(e) if isinstance(raw_str, str): try: - self.raw = (raw_str.lower() in true_values) + self.raw = raw_str.lower() in true_values except ValueError as e: logger.info(e) - if self.provider in config.providers.keys(): + if self.provider in config.providers: self.valid = True path = handler.request.path if self.provider.startswith("nextcloud"): @@ -82,7 +86,7 @@ class UpstreamHandler(): class MainHandler(tornado.web.RequestHandler): - async def handle_any(self, redir): + async def handle_any(self): handler = UpstreamHandler() await handler.setup(self) if handler.valid: @@ -102,18 +106,29 @@ class MainHandler(tornado.web.RequestHandler): if config.template_script is not None and config.template_html is not None: provider_data = None if handler.provider.startswith("nextcloud"): - provider_data = await stream_providers.get_nextcloud(handler.upstream, handler.proxy, logger) + provider_data = await stream_providers.get_nextcloud( + handler.upstream, handler.proxy, logger + ) elif handler.provider.startswith("seafile"): - provider_data = await stream_providers.get_seafile(handler.upstream, handler.proxy, logger) + provider_data = await stream_providers.get_seafile( + handler.upstream, handler.proxy, logger + ) else: - provider_data = await stream_providers.get_any(handler.upstream, handler.proxy, logger) + provider_data = await stream_providers.get_any( + handler.upstream, handler.proxy, logger + ) video_info = {} if handler.direct: video_info["upstream"] = provider_data.upstream() video_info["poster"] = provider_data.thumbnail() else: - proxied = await handler.proxy.proxy_url([(provider_data.upstream(), provider_data.proxy_ctype()), provider_data.thumbnail()]) + proxied = await handler.proxy.proxy_url( + [ + (provider_data.upstream(), provider_data.proxy_ctype()), + provider_data.thumbnail(), + ] + ) video_info["upstream"] = proxied[0] video_info["poster"] = proxied[1] @@ -136,22 +151,24 @@ class MainHandler(tornado.web.RequestHandler): script = config.template_script.generate(info=json.dumps(video_info)) b64 = str(base64.b64encode(script), "ascii") data = {} - script_file = f'data:text/javascript;charset=utf-8;base64,{b64}' + script_file = f"data:text/javascript;charset=utf-8;base64,{b64}" data["script"] = script_file data["videojs_version"] = config.videojs_version data["chromecast_version"] = config.chromecast_version data["font_awesome_version"] = config.font_awesome_version - rendered_html = config.template_html.generate(data=data, meta=meta, title=title) + rendered_html = config.template_html.generate( + data=data, meta=meta, title=title + ) self.write(rendered_html) else: self.set_status(404) self.write("HTML template missing.") async def get(self): - await self.handle_any(True) + await self.handle_any() async def head(self): - await self.handle_any(False) + await self.handle_any() def data_received(self, _): pass @@ -186,13 +203,32 @@ class StyleHandler(tornado.web.RequestHandler): try: handlers: list[tuple[tornado.routing.Matcher, tornado.web.RequestHandler]] = [] - handlers.append((cast(tornado.routing.Matcher, tornado.routing.PathMatches("/sources.m3u8")), cast(tornado.web.RequestHandler, PlaylistHandler))) - handlers.append((cast(tornado.routing.Matcher, tornado.routing.PathMatches("/favicon.ico")), cast(tornado.web.RequestHandler, IconHandler))) - handlers.append((cast(tornado.routing.Matcher, tornado.routing.PathMatches("/style.css")), cast(tornado.web.RequestHandler, StyleHandler))) - handlers.append((cast(tornado.routing.Matcher, tornado.routing.AnyMatches()), cast(tornado.web.RequestHandler, MainHandler))) + handlers.append( + ( + cast(tornado.routing.Matcher, tornado.routing.PathMatches("/sources.m3u8")), + cast(tornado.web.RequestHandler, PlaylistHandler), + ) + ) + handlers.append( + ( + cast(tornado.routing.Matcher, tornado.routing.PathMatches("/favicon.ico")), + cast(tornado.web.RequestHandler, IconHandler), + ) + ) + handlers.append( + ( + cast(tornado.routing.Matcher, tornado.routing.PathMatches("/style.css")), + cast(tornado.web.RequestHandler, StyleHandler), + ) + ) + handlers.append( + ( + cast(tornado.routing.Matcher, tornado.routing.AnyMatches()), + cast(tornado.web.RequestHandler, MainHandler), + ) + ) app_web = tornado.web.Application(cast(Any, handlers)) app_web.listen(8080) tornado.ioloop.IOLoop.current().start() except KeyboardInterrupt: print() - diff --git a/backend/stream_providers.py b/backend/stream_providers.py index d34c02b..db964a8 100755 --- a/backend/stream_providers.py +++ b/backend/stream_providers.py @@ -11,21 +11,26 @@ import json import re import config -class DummyLogger(): + +class DummyLogger: def debug(self, msg): pass + def warning(self, msg): pass + def error(self, msg): pass + class MetaParser(html.parser.HTMLParser): def __init__(self): self.meta_data = {} super().__init__() + def handle_starttag_meta(self, attrs): name = None - for attr in (attrs + attrs): + for attr in attrs + attrs: if len(attr) == 2: if isinstance(name, str): if attr[0] == "content": @@ -33,9 +38,10 @@ class MetaParser(html.parser.HTMLParser): return elif attr[0] == "property": name = attr[1] + def handle_starttag_input(self, attrs): name = None - for attr in (attrs + attrs): + for attr in attrs + attrs: if len(attr) == 2: if isinstance(name, str): if attr[0] == "value": @@ -43,14 +49,18 @@ class MetaParser(html.parser.HTMLParser): return elif attr[0] == "name": name = attr[1] + def handle_starttag(self, tag, attrs): if tag == "meta": return self.handle_starttag_meta(attrs) elif tag == "input": return self.handle_starttag_input(attrs) -class StreamData(): - def __init__(self, upstream, ctype, proxy_ctype, thumbnail, title, description, override): + +class StreamData: + def __init__( + self, upstream, ctype, proxy_ctype, thumbnail, title, description, override + ): self.values = {} self.values["upstream"] = upstream self.values["ctype"] = ctype @@ -59,13 +69,16 @@ class StreamData(): self.values["title"] = title self.values["description"] = description self.override = override + def update(self, key, value, override): missing = not isinstance(self.values.get(key), str) override = override and isinstance(value, str) if missing or override: self.values[key] = value + def upstream(self): return self.values.get("upstream") + def ctype(self): ctype = self.values.get("ctype") proxy_ctype = self.values.get("proxy_ctype") @@ -73,21 +86,28 @@ class StreamData(): if not ctype.startswith("audio/") and not ctype.startswith("video/"): return proxy_ctype return ctype + def proxy_ctype(self): return self.values.get("proxy_ctype") + def thumbnail(self): return self.values.get("thumbnail") + def title(self): return self.values.get("title") + def description(self): return self.values.get("description") + def complete(self): return None not in self.values.values() + def has_data(self): for elem in self.values.values(): if isinstance(elem, str): return True return False + def meta(self): data = [] if isinstance(self.values.get("thumbnail"), str): @@ -98,7 +118,8 @@ class StreamData(): data.append(("og:description", self.values.get("description"))) return data -class StreamProvider(): + +class StreamProvider: def __init__(self, upstream, proxy, logger): self.name = self.__class__.__name__ self.upstream = upstream @@ -106,6 +127,7 @@ class StreamProvider(): self.logger = logger if isinstance(proxy, config.ProxyElem): self.proxy = proxy + def extract_mime(self, upstream): try: url = urllib.parse.urlparse(upstream) @@ -121,6 +143,7 @@ class StreamProvider(): return "application/vnd.apple.mpegurl" return mime return None + def init_stream(self): stream = {} stream["upstream"] = None @@ -131,6 +154,7 @@ class StreamProvider(): stream["description"] = None stream["override"] = False return stream + def process(self): data = self.stream() proxy_ctype = data.proxy_ctype() @@ -144,7 +168,9 @@ class StreamProvider(): ctype = None upstream = data.upstream() try: - resp = requests.head(data.upstream(), proxies=proxies, timeout=5, allow_redirects=True) + resp = requests.head( + data.upstream(), proxies=proxies, timeout=5, allow_redirects=True + ) except Exception as e: self.logger.info("%s <%s>", e, self.upstream) else: @@ -155,7 +181,16 @@ class StreamProvider(): ctype = None elif "mpegurl" in ctype: ctype = "application/vnd.apple.mpegurl" - return StreamData(data.upstream(), ctype, proxy_ctype, data.thumbnail(), data.title(), data.description(), data.override) + return StreamData( + data.upstream(), + ctype, + proxy_ctype, + data.thumbnail(), + data.title(), + data.description(), + data.override, + ) + async def run(self): data = None try: @@ -165,6 +200,7 @@ class StreamProvider(): self.logger.info("%s <%s>", e, self.upstream) return data + class StreamlinkRunner(StreamProvider): def stream(self): try: @@ -184,11 +220,20 @@ class StreamlinkRunner(StreamProvider): for key in reversed(streams): stream = streams.get(key) if hasattr(stream, "url"): - return StreamData(stream.url, self.extract_mime(stream.url), None, None, None, None, False) + return StreamData( + stream.url, + self.extract_mime(stream.url), + None, + None, + None, + None, + False, + ) except Exception as e: self.logger.info("%s <%s>", e, self.upstream) return StreamData(None, None, None, None, None, None, False) + class YoutubeRunner(StreamProvider): def stream(self): best_stream = self.init_stream() @@ -216,15 +261,17 @@ class YoutubeRunner(StreamProvider): best_width = best_format.get("width") best_height = best_format.get("height") new_url = vformat.get("url") - if (isinstance(best_width, int) and - isinstance(best_height, int) and - isinstance(current_width, int) and - isinstance(current_height, int) and - isinstance(new_url, str) and - current_width > best_width and - current_height > best_height and - acodec != "none" and - vcodec != "none"): + if ( + isinstance(best_width, int) + and isinstance(best_height, int) + and isinstance(current_width, int) + and isinstance(current_height, int) + and isinstance(new_url, str) + and current_width > best_width + and current_height > best_height + and acodec != "none" + and vcodec != "none" + ): best_format = vformat best_stream["override"] = True best_stream["upstream"] = new_url @@ -233,11 +280,14 @@ class YoutubeRunner(StreamProvider): self.logger.info("%s <%s>", e, self.upstream) return StreamData(**best_stream) + class SeafileRunner(StreamProvider): def stream(self): stream_data = self.init_stream() json_data = None - proc = subprocess.run(["/app/seafile.js", self.upstream], capture_output=True, encoding="utf-8") + proc = subprocess.run( + ["/app/seafile.js", self.upstream], capture_output=True, encoding="utf-8" + ) try: json_data = json.loads(proc.stdout) except Exception as e: @@ -249,6 +299,7 @@ class SeafileRunner(StreamProvider): stream_data["proxy_ctype"] = "video/mp4" return StreamData(**stream_data) + class MetaProvider(StreamProvider): def parse_web(self): stream_data = self.init_stream() @@ -268,6 +319,7 @@ class MetaProvider(StreamProvider): stream_data["description"] = data.get("og:description") return stream_data + class MetaRunner(MetaProvider): def stream(self): stream_data = self.parse_web() @@ -275,14 +327,17 @@ class MetaRunner(MetaProvider): stream_data["ctype"] = None return StreamData(**stream_data) + class NextcloudRunner(MetaProvider): def stream(self): stream_data = self.parse_web() stream_data["thumbnail"] = None return StreamData(**stream_data) + upstream_cache = expiringdict.ExpiringDict(max_len=512, max_age_seconds=18000) + async def get_from_runner(cache_key, runner, logger): result = None cached = upstream_cache.get(cache_key) @@ -298,20 +353,36 @@ async def get_from_runner(cache_key, runner, logger): result = result_temp return result + async def get_streamlink(upstream, proxy, logger): - return await get_from_runner((0, upstream), StreamlinkRunner(upstream, proxy, logger), logger) + return await get_from_runner( + (0, upstream), StreamlinkRunner(upstream, proxy, logger), logger + ) + async def get_ytdl(upstream, proxy, logger): - return await get_from_runner((1, upstream), YoutubeRunner(upstream, proxy, logger), logger) + return await get_from_runner( + (1, upstream), YoutubeRunner(upstream, proxy, logger), logger + ) + async def get_meta(upstream, proxy, logger): - return await get_from_runner((2, upstream), MetaRunner(upstream, proxy, logger), logger) + return await get_from_runner( + (2, upstream), MetaRunner(upstream, proxy, logger), logger + ) + async def get_nextcloud(upstream, proxy, logger): - return await get_from_runner((3, upstream), NextcloudRunner(upstream, proxy, logger), logger) + return await get_from_runner( + (3, upstream), NextcloudRunner(upstream, proxy, logger), logger + ) + async def get_seafile(upstream, proxy, logger): - return await get_from_runner((3, upstream), SeafileRunner(upstream, proxy, logger), logger) + return await get_from_runner( + (3, upstream), SeafileRunner(upstream, proxy, logger), logger + ) + async def get_any(upstream, proxy, logger): cache_key = (4, upstream) @@ -331,7 +402,9 @@ async def get_any(upstream, proxy, logger): result.update("ctype", temp_result.ctype(), temp_result.override) result.update("thumbnail", temp_result.thumbnail(), temp_result.override) result.update("title", temp_result.title(), temp_result.override) - result.update("description", temp_result.description(), temp_result.override) + result.update( + "description", temp_result.description(), temp_result.override + ) if result.complete(): upstream_cache[cache_key] = result break