diff --git a/backend/stream.py b/backend/stream.py index af405b3..ab2b4bd 100755 --- a/backend/stream.py +++ b/backend/stream.py @@ -47,37 +47,17 @@ class ProxyElem(): return aiohttp.ClientSession(connector=connector, timeout=timeout) def __repr__(self): return str(self.proxy) - async def content_type(self, url): - cached = ctype_cache.get(url) - if isinstance(cached, str) and "binary" not in cached.lower(): - return cached - async with self.session() as session: - for i in range(5): - try: - resp = await session.head(url) - ctype = resp.headers.get("Content-Type", None) - except Exception as e: - logger.info(e) - else: - if isinstance(ctype, str): - ctype_cache[url] = ctype - return ctype - return "binary/octet-type" - async def proxy_url(self, current, path): + async def proxy_url(self, urls): + if not isinstance(proxy_server, str): + return urls jdata = None - data = {} - data_list = [data] - if path is None: - data["upstream"] = current - else: - data["upstream"] = urllib.parse.urljoin(current, path) - data["proxied"] = True - if self.proxy is None: - data["proxied"] = False - else: + data_list = [] + for url in urls: + data = {} + data["upstream"] = url data["proxy"] = self.proxy - if proxy_server is None: - return data["upstream"] + data["proxied"] = isinstance(self.proxy, str) + data_list.append(data) try: async with self.local() as session: resp = await session.post(proxy_server, json=data_list) @@ -85,10 +65,16 @@ class ProxyElem(): jdata = json.loads(text) except Exception as e: logger.info(e) - if isinstance(jdata, list) and len(jdata) == 1: - return jdata[0] + if isinstance(jdata, list): + ret_data = [] + for i in range(len(jdata)): + if isinstance(urls[i], str): + ret_data.append(jdata[i]) + else: + ret_data.append(None) + return ret_data else: - return data["upstream"] + return urls class AsyncSessionData(): def __init__(self, resp, current): @@ -125,28 +111,18 @@ stream_providers.setup(proxy_keys) class UpstreamHandler(): def __init__(self): self.provider = None - self.render_url = None - self.stream_url = None + self.valid = False self.proxy = None self.upstream = None - self.upstream_safe = None - self.render = False - self.stream = False async def setup(self, handler): self.provider = handler.get_query_argument("provider", None) - render_str = handler.get_query_argument("render", "false") if self.provider in providers.keys(): - if render_str.lower() == "true": - self.render = True - else: - self.stream = True - + self.valid = True path = handler.request.path if self.provider == "nextcloud": path = path.removesuffix("/").removesuffix("download").removesuffix("/") elif self.provider == "youtube": path = path.removeprefix("/") - src = providers[self.provider] + path proxy_list = proxies.get(self.provider) if isinstance(proxy_list, list): @@ -170,10 +146,8 @@ class UpstreamHandler(): new_url = str(resp.url) if new_url.lower().startswith("https://consent.youtube.com"): self.upstream = src - self.upstream_safe = urllib.parse.quote(src) else: self.upstream = new_url - self.upstream_safe = urllib.parse.quote(new_url) self.proxy = result.current break for future in futures: @@ -201,7 +175,7 @@ if icecast_server is not None and stream_server is not None: logger.info(e) template_html = None -script_file = None +template_script = None videojs_version = None font_awesome_version = None custom_style = None @@ -210,9 +184,7 @@ try: with open("/app/index.html", "r") as f: template_html = tornado.template.Template(f.read().strip()) with open("/app/script.js", "r") as f: - script_raw = bytes(f.read().strip(), "utf-8") - b64 = str(base64.b64encode(script_raw), "ascii") - script_file = f'data:text/javascript;charset=utf-8;base64,{b64}' + template_script = tornado.template.Template(f.read().strip()) with open("/app/version/video.js.txt", "r") as f: videojs_version = f.read().strip() with open("/app/version/chromecast.txt", "r") as f: @@ -232,18 +204,31 @@ class MainHandler(tornado.web.RequestHandler): async def handle_any(self, redir): handler = UpstreamHandler() await handler.setup(self) - if handler.render: + if handler.valid: await self.handle_render(handler) - elif handler.stream: - await self.handle_stream(handler, redir) else: logger.info(f'provider missing {self.request.uri}') self.set_status(404) self.write("Stream not found. (provider missing)") async def handle_render(self, handler): - if script_file is not None and template_html is not None: - provider_data = await stream_providers.get_any(handler.upstream, handler.proxy, logger) + if template_script is not None and template_html is not None: + provider_data = None + if handler.provider == "nextcloud": + provider_data = await stream_providers.get_nextcloud(handler.upstream, handler.proxy, logger) + else: + provider_data = await stream_providers.get_any(handler.upstream, handler.proxy, logger) + proxied = await handler.proxy.proxy_url([provider_data.upstream(), provider_data.thumbnail()]) + + video_info = {} + video_info["upstream"] = proxied[0] + video_info["poster"] = proxied[1] + video_info["ctype"] = provider_data.ctype() + + script = template_script.generate(info=json.dumps(video_info)) + b64 = str(base64.b64encode(script), "ascii") + script_file = f'data:text/javascript;charset=utf-8;base64,{b64}' + data["script"] = script_file data["videojs_version"] = videojs_version data["chromecast_version"] = chromecast_version @@ -255,35 +240,6 @@ class MainHandler(tornado.web.RequestHandler): self.set_status(404) self.write("HTML template missing.") - async def handle_stream(self, handler, redir): - upstream = None - if handler.provider == "nextcloud": - upstream = handler.upstream + "/download" - else: - provider_data = await stream_providers.get_any(handler.upstream, handler.proxy, logger) - upstream = provider_data.upstream() - if isinstance(provider_data.thumbnail(), str): - image = await handler.proxy.proxy_url(provider_data.thumbnail(), None) - if isinstance(image, str): - self.set_header("Custom-Poster", image) - if upstream is None: - logger.info(f'invalid upstream ({handler.provider})') - self.set_status(404) - self.write("Stream not found. (invalid upstream)") - else: - upstream_proxy = await handler.proxy.proxy_url(upstream, None) - logger.info(upstream_proxy) - ctype = await handler.proxy.content_type(upstream_proxy) - self.set_header("Content-Type", ctype) - if redir: - if ctype == "application/vnd.apple.mpegurl": - async with handler.proxy.local() as session: - resp = await session.get(upstream_proxy) - data = await resp.read() - self.write(data) - else: - self.redirect(upstream_proxy, status=303) - async def get(self): await self.handle_any(True) async def head(self): diff --git a/backend/stream_providers.py b/backend/stream_providers.py index b4a1a40..dc115de 100755 --- a/backend/stream_providers.py +++ b/backend/stream_providers.py @@ -4,6 +4,7 @@ import requests import asyncio import html.parser import expiringdict +import json streamlink_sessions = {} streamlink_default_session = streamlink.Streamlink() @@ -41,10 +42,41 @@ class MetaParser(html.parser.HTMLParser): elif attr[0] == "property" and attr[1] in self.accepted_attrs: name = attr[1] +class NextcloudParser(html.parser.HTMLParser): + def __init__(self): + self.meta_data = {} + super().__init__() + def handle_starttag_meta(self, attrs): + name = None + for attr in (attrs + attrs): + if len(attr) == 2: + if isinstance(name, str): + if attr[0] == "content": + self.meta_data[name] = attr[1] + return + elif attr[0] == "property": + name = attr[1] + def handle_starttag_input(self, attrs): + name = None + for attr in (attrs + attrs): + if len(attr) == 2: + if isinstance(name, str): + if attr[0] == "value": + self.meta_data[name] = attr[1] + 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, thumbnail, title, description, override): + def __init__(self, upstream, ctype, thumbnail, title, description, override): self.values = {} self.values["upstream"] = upstream + self.values["ctype"] = ctype self.values["thumbnail"] = thumbnail self.values["title"] = title self.values["description"] = description @@ -56,6 +88,8 @@ class StreamData(): self.values[key] = value def upstream(self): return self.values.get("upstream") + def ctype(self): + return self.values.get("ctype") def thumbnail(self): return self.values.get("thumbnail") def title(self): @@ -83,10 +117,27 @@ class StreamProvider(): proxy = str(proxy) if len(proxy) > 5: self.proxy = proxy + def process(self): + data = self.stream() + if not isinstance(data.upstream(), str): + return data + proxies = None + if isinstance(self.proxy, str): + proxies = {} + proxies["http"] = "socks5://" + self.proxy + proxies["https"] = "socks5://" + self.proxy + ctype = "binary/octet-stream" + try: + resp = requests.head(data.upstream(), proxies=proxies) + except Exception as e: + self.logger.info(e) + else: + ctype = resp.headers.get("Content-Type", "binary/octet-stream") + return StreamData(data.upstream(), ctype, data.thumbnail(), data.title(), data.description(), data.override) async def run(self): data = None try: - future = asyncio.to_thread(self.stream) + future = asyncio.to_thread(self.process) data = await asyncio.wait_for(future, timeout=5) except Exception as e: self.logger.info(e) @@ -106,10 +157,10 @@ class StreamlinkRunner(StreamProvider): for key in reversed(streams): stream = streams.get(key) if hasattr(stream, "url"): - return StreamData(stream.url, None, None, None, False) + return StreamData(stream.url, None, None, None, None, False) except Exception as e: self.logger.info(e) - return StreamData(None, None, None, None, False) + return StreamData(None, None, None, None, None, False) class YoutubeRunner(StreamProvider): def stream(self): @@ -153,7 +204,20 @@ class YoutubeRunner(StreamProvider): best_url = new_url except Exception as e: self.logger.info(e) - return StreamData(best_url, thumbnail, title, description, True) + print(json.dumps(best_format, indent=4)) + return StreamData(best_url, None, thumbnail, title, description, True) + +class NextcloudRunner(StreamProvider): + def stream(self): + data = {} + try: + resp = requests.get(self.upstream) + parser = NextcloudParser() + parser.feed(resp.text) + data = parser.meta_data + except Exception as e: + self.logger.info(e) + return StreamData(data.get("downloadURL"), data.get("mimetype"), None, data.get("og:title"), data.get("og:description"), False) class MetaRunner(StreamProvider): def stream(self): @@ -165,7 +229,7 @@ class MetaRunner(StreamProvider): data = parser.meta_data except Exception as e: self.logger.info(e) - return StreamData(None, data.get("og:image"), data.get("og:title"), data.get("og:description"), False) + return StreamData(None, None, data.get("og:image"), data.get("og:title"), data.get("og:description"), False) upstream_cache = expiringdict.ExpiringDict(max_len=512, max_age_seconds=1800) @@ -192,8 +256,11 @@ async def get_ytdl(upstream, proxy, logger): async def get_meta(upstream, proxy, 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) + async def get_any(upstream, proxy, logger): - cache_key = (3, upstream) + cache_key = (4, upstream) cached = upstream_cache.get(cache_key) if isinstance(cached, StreamData): return cached @@ -201,7 +268,8 @@ async def get_any(upstream, proxy, logger): tasks.append(asyncio.create_task(get_streamlink(upstream, proxy, logger))) tasks.append(asyncio.create_task(get_ytdl(upstream, proxy, logger))) tasks.append(asyncio.create_task(get_meta(upstream, proxy, logger))) - result = StreamData(None, None, None, None, False) + + result = StreamData(None, None, None, None, None, False) for task in asyncio.as_completed(tasks): temp_result = await task if isinstance(temp_result, StreamData): diff --git a/frontend/index.html b/frontend/index.html index aff73fb..3103bad 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12,6 +12,4 @@ - - diff --git a/frontend/script.js b/frontend/script.js index c957381..89b201e 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -1,33 +1,10 @@ (() => { - const findUpstreamVideo = () => { - const search = new URLSearchParams(location.search); - search.set("render", "false"); - const url = new URL(location.origin); - url.pathname = location.pathname; - url.search = search.toString(); - return url.href; - } - - const upstream = findUpstreamVideo(); - const xhr = new XMLHttpRequest(); - xhr.open("HEAD", upstream, true); - xhr.send(); - - let count = 2; - const handleCount = () => { - if(--count === 0) { - handle(); - } - } - + const info = {% raw info %}; const handle = () => { const [body] = document.getElementsByTagName("body"); const video = document.createElement("video"); video.className = "video-js vjs-big-play-centered"; body.appendChild(video); - const ctype = xhr.getResponseHeader("Content-Type"); - const image = xhr.getResponseHeader("Custom-Poster"); - console.log(ctype); const options = {}; options.controls = true; options.liveui = true; @@ -38,14 +15,14 @@ options.plugins.chromecast = {}; options.plugins.chromecast.addButtonToControlBar = false; const player = videojs(video, options); - if((image instanceof String) || ((typeof image) == "string")) { - player.poster(image); + if((info.poster instanceof String) || ((typeof info.poster) == "string")) { + player.poster(info.poster); } const source = {}; - source.type = ctype; - source.src = upstream; + source.type = info.ctype; + source.src = info.upstream; player.src(source); - const canPlayTypeRaw = player.canPlayType(ctype); + const canPlayTypeRaw = player.canPlayType(info.ctype); const canPlayType = (canPlayTypeRaw === "maybe") || (canPlayTypeRaw === "probably"); if(canPlayType) { const Button = videojs.getComponent("Button"); @@ -84,6 +61,5 @@ }); } } - document.addEventListener("DOMContentLoaded", handleCount); - xhr.addEventListener("load", handleCount); + document.addEventListener("DOMContentLoaded", handle); })();