load video from render

This commit is contained in:
Roy Olav Purser 2021-05-31 12:59:25 +02:00
parent a605e7c43d
commit 5f56cf22ae
Signed by: roypur
GPG Key ID: E14D26A036F21656
4 changed files with 123 additions and 125 deletions

View File

@ -47,37 +47,17 @@ class ProxyElem():
return aiohttp.ClientSession(connector=connector, timeout=timeout) return aiohttp.ClientSession(connector=connector, timeout=timeout)
def __repr__(self): def __repr__(self):
return str(self.proxy) return str(self.proxy)
async def content_type(self, url): async def proxy_url(self, urls):
cached = ctype_cache.get(url) if not isinstance(proxy_server, str):
if isinstance(cached, str) and "binary" not in cached.lower(): return urls
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):
jdata = None jdata = None
data_list = []
for url in urls:
data = {} data = {}
data_list = [data] data["upstream"] = url
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["proxy"] = self.proxy data["proxy"] = self.proxy
if proxy_server is None: data["proxied"] = isinstance(self.proxy, str)
return data["upstream"] data_list.append(data)
try: try:
async with self.local() as session: async with self.local() as session:
resp = await session.post(proxy_server, json=data_list) resp = await session.post(proxy_server, json=data_list)
@ -85,10 +65,16 @@ class ProxyElem():
jdata = json.loads(text) jdata = json.loads(text)
except Exception as e: except Exception as e:
logger.info(e) logger.info(e)
if isinstance(jdata, list) and len(jdata) == 1: if isinstance(jdata, list):
return jdata[0] ret_data = []
for i in range(len(jdata)):
if isinstance(urls[i], str):
ret_data.append(jdata[i])
else: else:
return data["upstream"] ret_data.append(None)
return ret_data
else:
return urls
class AsyncSessionData(): class AsyncSessionData():
def __init__(self, resp, current): def __init__(self, resp, current):
@ -125,28 +111,18 @@ stream_providers.setup(proxy_keys)
class UpstreamHandler(): class UpstreamHandler():
def __init__(self): def __init__(self):
self.provider = None self.provider = None
self.render_url = None self.valid = False
self.stream_url = None
self.proxy = None self.proxy = None
self.upstream = None self.upstream = None
self.upstream_safe = None
self.render = False
self.stream = False
async def setup(self, handler): async def setup(self, handler):
self.provider = handler.get_query_argument("provider", None) self.provider = handler.get_query_argument("provider", None)
render_str = handler.get_query_argument("render", "false")
if self.provider in providers.keys(): if self.provider in providers.keys():
if render_str.lower() == "true": self.valid = True
self.render = True
else:
self.stream = True
path = handler.request.path path = handler.request.path
if self.provider == "nextcloud": if self.provider == "nextcloud":
path = path.removesuffix("/").removesuffix("download").removesuffix("/") path = path.removesuffix("/").removesuffix("download").removesuffix("/")
elif self.provider == "youtube": elif self.provider == "youtube":
path = path.removeprefix("/") path = path.removeprefix("/")
src = providers[self.provider] + path src = providers[self.provider] + path
proxy_list = proxies.get(self.provider) proxy_list = proxies.get(self.provider)
if isinstance(proxy_list, list): if isinstance(proxy_list, list):
@ -170,10 +146,8 @@ class UpstreamHandler():
new_url = str(resp.url) new_url = str(resp.url)
if new_url.lower().startswith("https://consent.youtube.com"): if new_url.lower().startswith("https://consent.youtube.com"):
self.upstream = src self.upstream = src
self.upstream_safe = urllib.parse.quote(src)
else: else:
self.upstream = new_url self.upstream = new_url
self.upstream_safe = urllib.parse.quote(new_url)
self.proxy = result.current self.proxy = result.current
break break
for future in futures: for future in futures:
@ -201,7 +175,7 @@ if icecast_server is not None and stream_server is not None:
logger.info(e) logger.info(e)
template_html = None template_html = None
script_file = None template_script = None
videojs_version = None videojs_version = None
font_awesome_version = None font_awesome_version = None
custom_style = None custom_style = None
@ -210,9 +184,7 @@ try:
with open("/app/index.html", "r") as f: with open("/app/index.html", "r") as f:
template_html = tornado.template.Template(f.read().strip()) template_html = tornado.template.Template(f.read().strip())
with open("/app/script.js", "r") as f: with open("/app/script.js", "r") as f:
script_raw = bytes(f.read().strip(), "utf-8") template_script = tornado.template.Template(f.read().strip())
b64 = str(base64.b64encode(script_raw), "ascii")
script_file = f'data:text/javascript;charset=utf-8;base64,{b64}'
with open("/app/version/video.js.txt", "r") as f: with open("/app/version/video.js.txt", "r") as f:
videojs_version = f.read().strip() videojs_version = f.read().strip()
with open("/app/version/chromecast.txt", "r") as f: with open("/app/version/chromecast.txt", "r") as f:
@ -232,18 +204,31 @@ class MainHandler(tornado.web.RequestHandler):
async def handle_any(self, redir): async def handle_any(self, redir):
handler = UpstreamHandler() handler = UpstreamHandler()
await handler.setup(self) await handler.setup(self)
if handler.render: if handler.valid:
await self.handle_render(handler) await self.handle_render(handler)
elif handler.stream:
await self.handle_stream(handler, redir)
else: else:
logger.info(f'provider missing {self.request.uri}') logger.info(f'provider missing {self.request.uri}')
self.set_status(404) self.set_status(404)
self.write("Stream not found. (provider missing)") self.write("Stream not found. (provider missing)")
async def handle_render(self, handler): async def handle_render(self, handler):
if script_file is not None and template_html is not None: 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) 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["script"] = script_file
data["videojs_version"] = videojs_version data["videojs_version"] = videojs_version
data["chromecast_version"] = chromecast_version data["chromecast_version"] = chromecast_version
@ -255,35 +240,6 @@ class MainHandler(tornado.web.RequestHandler):
self.set_status(404) self.set_status(404)
self.write("HTML template missing.") 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): async def get(self):
await self.handle_any(True) await self.handle_any(True)
async def head(self): async def head(self):

View File

@ -4,6 +4,7 @@ import requests
import asyncio import asyncio
import html.parser import html.parser
import expiringdict import expiringdict
import json
streamlink_sessions = {} streamlink_sessions = {}
streamlink_default_session = streamlink.Streamlink() 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: elif attr[0] == "property" and attr[1] in self.accepted_attrs:
name = attr[1] 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(): class StreamData():
def __init__(self, upstream, thumbnail, title, description, override): def __init__(self, upstream, ctype, thumbnail, title, description, override):
self.values = {} self.values = {}
self.values["upstream"] = upstream self.values["upstream"] = upstream
self.values["ctype"] = ctype
self.values["thumbnail"] = thumbnail self.values["thumbnail"] = thumbnail
self.values["title"] = title self.values["title"] = title
self.values["description"] = description self.values["description"] = description
@ -56,6 +88,8 @@ class StreamData():
self.values[key] = value self.values[key] = value
def upstream(self): def upstream(self):
return self.values.get("upstream") return self.values.get("upstream")
def ctype(self):
return self.values.get("ctype")
def thumbnail(self): def thumbnail(self):
return self.values.get("thumbnail") return self.values.get("thumbnail")
def title(self): def title(self):
@ -83,10 +117,27 @@ class StreamProvider():
proxy = str(proxy) proxy = str(proxy)
if len(proxy) > 5: if len(proxy) > 5:
self.proxy = proxy 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): async def run(self):
data = None data = None
try: try:
future = asyncio.to_thread(self.stream) future = asyncio.to_thread(self.process)
data = await asyncio.wait_for(future, timeout=5) data = await asyncio.wait_for(future, timeout=5)
except Exception as e: except Exception as e:
self.logger.info(e) self.logger.info(e)
@ -106,10 +157,10 @@ class StreamlinkRunner(StreamProvider):
for key in reversed(streams): for key in reversed(streams):
stream = streams.get(key) stream = streams.get(key)
if hasattr(stream, "url"): 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: except Exception as e:
self.logger.info(e) self.logger.info(e)
return StreamData(None, None, None, None, False) return StreamData(None, None, None, None, None, False)
class YoutubeRunner(StreamProvider): class YoutubeRunner(StreamProvider):
def stream(self): def stream(self):
@ -153,7 +204,20 @@ class YoutubeRunner(StreamProvider):
best_url = new_url best_url = new_url
except Exception as e: except Exception as e:
self.logger.info(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): class MetaRunner(StreamProvider):
def stream(self): def stream(self):
@ -165,7 +229,7 @@ class MetaRunner(StreamProvider):
data = parser.meta_data data = parser.meta_data
except Exception as e: except Exception as e:
self.logger.info(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) 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): 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)
async def get_any(upstream, proxy, logger): async def get_any(upstream, proxy, logger):
cache_key = (3, upstream) cache_key = (4, upstream)
cached = upstream_cache.get(cache_key) cached = upstream_cache.get(cache_key)
if isinstance(cached, StreamData): if isinstance(cached, StreamData):
return cached 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_streamlink(upstream, proxy, logger)))
tasks.append(asyncio.create_task(get_ytdl(upstream, proxy, logger))) tasks.append(asyncio.create_task(get_ytdl(upstream, proxy, logger)))
tasks.append(asyncio.create_task(get_meta(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): for task in asyncio.as_completed(tasks):
temp_result = await task temp_result = await task
if isinstance(temp_result, StreamData): if isinstance(temp_result, StreamData):

View File

@ -12,6 +12,4 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/{{ data["font_awesome_version"] }}/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/{{ data["font_awesome_version"] }}/css/all.min.css">
<link rel="stylesheet" href="{{ data["custom_style"] }}"> <link rel="stylesheet" href="{{ data["custom_style"] }}">
</head> </head>
<body>
</body>
</html> </html>

View File

@ -1,33 +1,10 @@
(() => { (() => {
const findUpstreamVideo = () => { const info = {% raw info %};
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 handle = () => { const handle = () => {
const [body] = document.getElementsByTagName("body"); const [body] = document.getElementsByTagName("body");
const video = document.createElement("video"); const video = document.createElement("video");
video.className = "video-js vjs-big-play-centered"; video.className = "video-js vjs-big-play-centered";
body.appendChild(video); body.appendChild(video);
const ctype = xhr.getResponseHeader("Content-Type");
const image = xhr.getResponseHeader("Custom-Poster");
console.log(ctype);
const options = {}; const options = {};
options.controls = true; options.controls = true;
options.liveui = true; options.liveui = true;
@ -38,14 +15,14 @@
options.plugins.chromecast = {}; options.plugins.chromecast = {};
options.plugins.chromecast.addButtonToControlBar = false; options.plugins.chromecast.addButtonToControlBar = false;
const player = videojs(video, options); const player = videojs(video, options);
if((image instanceof String) || ((typeof image) == "string")) { if((info.poster instanceof String) || ((typeof info.poster) == "string")) {
player.poster(image); player.poster(info.poster);
} }
const source = {}; const source = {};
source.type = ctype; source.type = info.ctype;
source.src = upstream; source.src = info.upstream;
player.src(source); player.src(source);
const canPlayTypeRaw = player.canPlayType(ctype); const canPlayTypeRaw = player.canPlayType(info.ctype);
const canPlayType = (canPlayTypeRaw === "maybe") || (canPlayTypeRaw === "probably"); const canPlayType = (canPlayTypeRaw === "maybe") || (canPlayTypeRaw === "probably");
if(canPlayType) { if(canPlayType) {
const Button = videojs.getComponent("Button"); const Button = videojs.getComponent("Button");
@ -84,6 +61,5 @@
}); });
} }
} }
document.addEventListener("DOMContentLoaded", handleCount); document.addEventListener("DOMContentLoaded", handle);
xhr.addEventListener("load", handleCount);
})(); })();