Compare commits

...

24 Commits

Author SHA1 Message Date
lamp 669ea6a41b Merge branch 'master' into nginx 2023-07-31 19:35:58 -07:00
lamp b2a3c5a048 fix for missing header 2023-07-31 18:39:23 -07:00
lamp 5a91e654f8 cache prune thread is daemon 2023-07-28 13:34:20 -07:00
lamp 3a19f8fe72 support music.youtube.com 2023-07-28 00:37:33 -07:00
lamp b49918f8a6 Update 'README.md' 2023-07-28 02:29:21 -05:00
lamp a79ee967d2 Update 'README.md' 2023-07-23 02:36:00 -05:00
lamp cddbbcc866 fix expire 2023-07-22 23:25:56 -07:00
lamp a007796147 total bypass from pc vrchat with id 2023-07-22 23:09:56 -07:00
lamp 83690c4535 caddy formatted log 2023-07-22 21:35:03 -07:00
lamp 8057cdb44d nginx
maybe uses less cpu idk
2023-07-22 21:07:26 -07:00
lamp 8364c23c5f mount app instead of build into image 2023-07-22 21:05:26 -07:00
lamp 510cca1b3a caddy rewrite google 302 2023-07-22 20:51:03 -07:00
lamp 14e5fe6df7 fix cache prune 2023-07-22 20:26:30 -07:00
lamp 879661a2ac docker 2023-07-22 00:00:06 -07:00
lamp c5e2eb3c37 cache prune interval 2023-07-21 23:59:45 -07:00
lamp f9fab223e8 bypass pc vrchat 2023-07-21 13:07:48 -07:00
lamp 29a951f4fa improve selection erroring 2023-06-22 21:55:01 -07:00
lamp 81ef6df1cf display error as video 2023-06-22 20:47:07 -07:00
lamp 5ea0cccc13 custom send_error 2023-06-21 16:07:35 -07:00
lamp d2c18a0814 delete www 2023-06-21 14:56:11 -07:00
lamp b9f304dc01 add alt 2023-03-28 17:45:12 -05:00
lamp 7528b6e76b add alternate 2023-03-28 14:55:56 -05:00
lamp c65c254d80 Merge branch 'master' of gitea.moe:lamp/u2b.cx 2023-03-27 13:45:16 -07:00
lamp 99b6000b9b add favicon.ico 2023-03-27 13:45:13 -07:00
16 changed files with 312 additions and 162 deletions
+3 -1
View File
@@ -1 +1,3 @@
.vscode .vscode
__pycache__
test.mp4
-38
View File
@@ -1,38 +0,0 @@
u2b.cx {
import common
route {
@www path / /favicon.ico
redir @www https://www.u2b.cx{uri} permanent
respond /robots.txt "User-agent: *
Disallow: /
"
respond /.* 403
@notget not method GET
respond @notget 403
reverse_proxy http://127.29.151.200:52482
}
}
proxy.u2b.cx {
import common
@gv path_regexp gvurl ^\/([a-z0-9-]+\.googlevideo\.com)
handle @gv {
uri strip_prefix /{re.gvurl.1}
#uri strip_prefix {re.gvurl.0} doesn't work for some reason despite that value being equal to /{re.gvurl.1}
reverse_proxy {
to {re.gvurl.1}:443
header_up Host {re.gvurl.1}
transport http {
tls
}
}
}
}
www.u2b.cx {
import common
root * /srv/u2b.cx/www/
file_server
}
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.11
RUN useradd -r -m u2b
RUN apt update && apt install -y ffmpeg
RUN pip install --no-cache-dir python-ffmpeg yt-dlp
#COPY . /app
#WORKDIR /app
USER u2b
#ENV PORT=8080
#CMD ["python", "server.py"]
+21 -4
View File
@@ -1,8 +1,12 @@
# u2b.cx # u2b.cx
A YouTube video file resolver thingy via search query, made to let you watch YouTube in Quest VRChat where there aren't really any other options to do so. A YouTube search resolver + raw file resolver w/ proxy for Quest VRChat.
## Features - Get the video you want just by typing its name in the URL
- Video works for both PC and Quest VRChat
- Proxying avoids random blocks from google's servers
## Technical Features
- Written in Python to integrate with YoutubeDL (yt-dlp) for fastest performance - Written in Python to integrate with YoutubeDL (yt-dlp) for fastest performance
- Multi-threaded for concurrent usage - Multi-threaded for concurrent usage
@@ -10,6 +14,8 @@ A YouTube video file resolver thingy via search query, made to let you watch You
- Limited to one YoutubeDL invocation per IP address - Limited to one YoutubeDL invocation per IP address
- Results cached for 5 hours or until expiry found in extracted URL - Results cached for 5 hours or until expiry found in extracted URL
- Extracted URLs proxied in Caddy so that they work in all countries - Extracted URLs proxied in Caddy so that they work in all countries
- Errors displayed as a 10 second single-frame video
- PC VRchat bypassed to save bandwidth (todo: sacrifices consistency)
### Planned ### Planned
@@ -20,8 +26,19 @@ A YouTube video file resolver thingy via search query, made to let you watch You
### GET `https://u2b.cx/<query>` ### GET `https://u2b.cx/<query>`
The server will search YouTube for `<query>`, pick the first result, pick the best quality all-in-one MP4 format available, and respond with a 302 redirect to the proxied raw MP4 file. The server will search YouTube for `<query>`, pick the first result, pick the best quality all-in-one MP4 format available, and respond with a 302 redirect to the proxied raw MP4 file. If the client is PC VRChat, the server may instead redirect to the YouTube video URL to save bandwidth on the server.
NOTE: query must not start with a dot (.)
### GET `https://u2b.cx/id/<video id>` ### GET `https://u2b.cx/id/<video id>`
### GET `https://u2b.cx/https://www.youtube.com/watch?v=<video id>`
### GET `https://u2b.cx/https://youtu.be/<video id>`
### GET `https://u2b.cx/https://www.youtube.com/shorts/<video id>`
### GET `https://u2b.cx/https://music.youtube.com/watch?v=<video id>`
### etcetera...
Bypasses search to serve the video directly. Bypasses search to look up the video directly by its id. If the client is PC VRChat, it may be immediately redirected to the YouTube url to save resources on the server.
Regex only matches the start of the string; anything after the 11-char video id is ignored.
Malformed YouTube URLs will be treated as a YouTube search query and YouTube search will probably give what you want.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

+51
View File
@@ -0,0 +1,51 @@
{
log http.log.access {
include http.log.access
output stdout
format formatted "[{ts}] {request>remote_ip} {request>headers>X-Forwarded-For} {request>method} {request>host}{request>uri} {status} {request>headers>User-Agent} {request>headers>Referer}" {
time_format "02/Jan/2006:15:04:05-0700"
}
}
log default {
exclude http.log.access
output stderr
format console
}
}
:80 {
#nl.u2b.cx u2b.cx {
log
handle_path /proxy/* {
@gv path_regexp gvurl ^\/([a-z0-9-]+\.googlevideo\.com)
handle @gv {
uri strip_prefix /{re.gvurl.1}
reverse_proxy {
to {re.gvurl.1}:443
header_up Host {re.gvurl.1}
header_down Location "^https://(.*)" "/proxy/$1"
transport http {
tls
}
}
}
handle {
respond 403
}
}
handle {
route {
@www path / /favicon.ico
redir @www https://www.u2b.cx{uri} permanent
respond /robots.txt "User-agent: *
Disallow: /
"
respond /.* 403
@notget not method GET
respond @notget 403
reverse_proxy http://app:8080
}
}
}
+4
View File
@@ -0,0 +1,4 @@
FROM caddy:2.6-builder AS builder
RUN xcaddy build --with github.com/caddyserver/transform-encoder
FROM caddy:2.6
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
+36
View File
@@ -0,0 +1,36 @@
version: "3.8"
name: "u2bcx"
services:
app:
build: .
restart: always
volumes:
- ./:/app/
working_dir: /app/
environment:
- PORT=8080
- PROXY=/proxy/
command: python server.py
# caddy:
# image: caddy:2.6
# restart: always
# ports:
# - "80:80"
# - "443:443"
# - "443:443/udp"
# volumes:
# - ./Caddyfile:/etc/caddy/Caddyfile
# - caddy_data:/data
# - caddy_config:/config
nginx:
image: nginx:1.25
restart: always
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
caddy_data:
caddy_config:
+56
View File
@@ -0,0 +1,56 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$time_local $remote_addr "$request" $status "$http_user_agent"';
access_log /var/log/nginx/access.log main;
resolver 8.8.8.8 ipv6=off;#until I can get ipv6 on the new host
server {
listen 80;
location /proxy/ {
limit_except GET { deny all; }
location ~^/proxy/([a-z0-9-]+)\.googlevideo\.com/videoplayback {
#return 200 "$uri\n\n$request_uri\n\n$query_string\n\nhttps://$1/$2";
proxy_pass https://$1.googlevideo.com/videoplayback?$query_string;
#proxy_redirect ~https://([a-z0-9-]+).googlevideo.com/ /proxy/$1.googlevideo.com/;
proxy_redirect https:// /proxy/;
}
return 403;
}
location = / {
return 301 https://www.u2b.cx/;
}
location = /favicon.ico {
return 404;
}
location /. {
return 403;
}
location = /robots.txt {
add_header Content-Type text/plain;
return 200 "User-agent: *\nDisallow: /\n";
}
if ($request_method != GET) {
return 403;
}
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://app:8080;
}
}
}
BIN
View File
Binary file not shown.
+88 -36
View File
@@ -1,77 +1,129 @@
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from importlib.metadata import version
print("yt-dlp version", version("yt_dlp"))
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse, parse_qs from urllib.parse import unquote, urlparse, parse_qs
from threading import Event from threading import Event, Thread
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep
from os import environ from os import environ
import logging import logging
import re import re
from textvid import generate_video_from_text
def getBestMp4UrlFromInfo(info): ctx_cache = {}
formats = info['entries'][0].get('formats') if info.get("entries") else info.get("formats")
if not formats: return None
valid = list(filter(lambda x: x['ext'] == "mp4" and x['vcodec'] != 'none' and x['acodec'] != 'none', formats))
if not valid: return None
best = max(valid, key=lambda x: x['height'])
return best.get('url')
def getExpireDatetime(url):
alt_expire = datetime.now() + timedelta(hours=5)
if not url: return alt_expire
q = parse_qs(urlparse(url).query)
expire = q.get('expire')
if not expire: return alt_expire
expire = datetime.fromtimestamp(int(expire[0])) #this seems to always be +6 hours
return min([expire, alt_expire])
query_dicts = {}
ips_running_ytdl = [] ips_running_ytdl = []
def cache_prune_loop():
while True:
sleep(3600)
for key in ctx_cache:
if datetime.now() >= ctx_cache[key]['expire']:
del ctx_cache[key]
Thread(target=cache_prune_loop, daemon=True).start()
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
def address_string(self): def address_string(self):
return getattr(self, 'headers', {}).get('X-Forwarded-For', '').split(',')[0] or self.client_address[0] return getattr(self, 'headers', {}).get('X-Forwarded-For', '').split(',')[0] or self.client_address[0]
def is_pc_vrchat(self):
ua = self.headers.get('User-Agent', '')
ae = self.headers.get('Accept-Encoding', '')
return ua.startswith("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/") and ua.endswith(" Safari/537.36") and ae == "identity"
def send_error(self, code, message=""):
body = bytes(message, "utf-8")
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self): def do_GET(self):
# block other bot junk in reverse proxy # block other bot junk in reverse proxy
if self.path in ["/", "/favicon.ico"]: if self.path in ["/", "/favicon.ico"] or self.path.startswith("/."):
self.send_error(404) self.send_error(404)
return return
path = unquote(self.path) path = unquote(self.path)
match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path) match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.|music\.|m\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path)
query = match[1] if match else "ytsearch:" + path[1:] if match:
if self.is_pc_vrchat():
self.send_response(302)
self.send_header("Location", "https://www.youtube.com/watch?v=" + match[1])
self.end_headers()
return
query = match[1]
else:
query = "ytsearch:" + path[1:]
dict = query_dicts.get(query) ctx = ctx_cache.get(query)
if not dict or 'expire' in dict and datetime.now() >= dict['expire']: if not ctx or 'expire' in ctx and datetime.now() >= ctx['expire']:
client_ip = self.address_string() client_ip = self.address_string()
if client_ip in ips_running_ytdl: if client_ip in ips_running_ytdl:
self.send_error(429) self.send_error(429)
return return
try: try:
ips_running_ytdl.append(client_ip) ips_running_ytdl.append(client_ip)
query_dicts[query] = dict = {'event': Event()} ctx_cache[query] = ctx = {
'event': Event(),
'expire': datetime.now() + timedelta(hours=5)
}
with YoutubeDL() as ydl: with YoutubeDL() as ydl:
info = ydl.extract_info(query, download=False) info = ydl.extract_info(query, download=False)
url = getBestMp4UrlFromInfo(info)
dict['url'] = url selection = info
dict['expire'] = getExpireDatetime(url) if "entries" in info:
if not info["entries"]:
raise Exception("ERROR: No videos found!")
else:
selection = info["entries"][0]
ctx['id'] = selection['id']
suitable_formats = list(filter(lambda x: x['ext'] == "mp4" and x['vcodec'] != 'none' and x['acodec'] != 'none', selection["formats"]))
if not suitable_formats:
raise Exception(f"ERROR: {selection['id']}: No suitable formats of this video available!")
best_format = max(suitable_formats, key=lambda x: x['height'])
ctx['url'] = best_format['url']
expire = parse_qs(urlparse(best_format['url']).query).get('expire', [])[0]
if expire:
expire = datetime.fromtimestamp(int(expire))
if expire < ctx['expire']: ctx['expire'] = expire
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
dict['exception'] = e ctx['exception'] = e
ctx['error_vid'] = generate_video_from_text(re.sub("(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]", '', str(e)))
finally: finally:
ips_running_ytdl.remove(client_ip) ips_running_ytdl.remove(client_ip)
dict['event'].set() ctx['event'].set()
elif 'url' not in dict: elif 'url' not in ctx:
dict['event'].wait(60) ctx['event'].wait(60)
if not dict.get('url'): if self.is_pc_vrchat():
if 'exception' in dict: if ctx.get('id'):
self.send_error(500, message=str(dict['exception']), explain="An unexpected exception was encountered while resolving this query") self.send_response(302)
self.send_header("Location", "https://www.youtube.com/watch?v=" + ctx['id'])
self.end_headers()
return
if not ctx.get('url'):
if 'exception' in ctx:
if 'error_vid' in ctx:
self.send_response(200)
self.send_header("Content-Type", "video/mp4")
self.send_header("Content-Length", str(len(ctx['error_vid'])))
self.end_headers()
self.wfile.write(ctx['error_vid'])
else:
self.send_error(500, message=str(ctx['exception']))
else: else:
self.send_error(404) self.send_error(404)
else: else:
url = dict['url'] url = ctx['url']
if 'PROXY' in environ: if 'PROXY' in environ:
url = environ['PROXY'] + url.replace("https://",'') url = environ['PROXY'] + url.replace("https://",'')
self.send_response(302) self.send_response(302)
+44
View File
@@ -0,0 +1,44 @@
from ffmpeg import FFmpeg
import textwrap
from tempfile import mktemp
from os import remove
def generate_video_from_text(text, test=False) -> bytes:
""" generate a single-frame ten-second mp4 displaying the text """
text = text.replace("\\", "\\\\").replace('"', '""').replace("'", "''").replace("%", "\\%").replace(":", "\\:")
text = textwrap.fill(text, 90)
file = mktemp()
peg = FFmpeg().option("y").input("bg.png", {
"framerate": "0.1"
}).output(file, {
"f": "mp4",
"t": "10",
"c:v": "libx264",
"pix_fmt": "yuv420p",
"vf": "drawtext=font=monospace:fontsize=24:x=10:y=10:text='"+text+"':"
})
if test:
@peg.on("start")
def on_start(arguments): print("cmd:", ' '.join(arguments))
@peg.on("stderr")
def on_stderr(line): print("stderr:", line)
peg.execute()
fp = open(file, "rb")
data = fp.read()
fp.close()
remove(file)
return data
if __name__ == "__main__":
print("test")
v = generate_video_from_text("""Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut sem viverra aliquet eget sit amet. Senectus et netus et malesuada fames ac. Gravida quis blandit turpis cursus in hac habitasse platea. Sed ullamcorper morbi tincidunt ornare massa eget egestas purus. Tristique risus nec feugiat in. Malesuada bibendum arcu vitae elementum curabitur vitae nunc sed velit. Porta lorem mollis aliquam ut porttitor leo a. Tellus rutrum tellus pellentesque eu tincidunt. Enim diam vulputate ut pharetra sit amet. Platea dictumst vestibulum rhoncus est. Sed sed risus pretium quam vulputate dignissim suspendisse. Viverra maecenas accumsan lacus vel facilisis volutpat est velit egestas. Lorem ipsum dolor sit amet consectetur. Netus et malesuada fames ac turpis egestas integer eget. Tellus elementum sagittis vitae et leo duis ut. Ipsum a arcu cursus vitae. Amet aliquam id diam maecenas ultricies mi. Mattis vulputate enim nulla aliquet porttitor lacus luctus accumsan. Magna ac placerat vestibulum lectus mauris ultrices eros in.
Dui sapien eget mi proin sed libero enim sed faucibus. Hac habitasse platea dictumst quisque sagittis purus sit. Mi eget mauris pharetra et ultrices neque ornare. Sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae. Eget arcu dictum varius duis. Purus in massa tempor nec feugiat nisl pretium. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Adipiscing diam donec adipiscing tristique risus. Pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Non quam lacus suspendisse faucibus.
Sed libero enim sed faucibus. Ut etiam sit amet nisl purus in mollis nunc sed. Cursus eget nunc scelerisque viverra mauris in aliquam sem fringilla. Eget aliquet nibh praesent tristique magna sit amet purus. Dui accumsan sit amet nulla facilisi morbi tempus. Lacus laoreet non curabitur gravida. Mi eget mauris pharetra et ultrices neque. Volutpat est velit egestas dui id ornare arcu odio. Porttitor lacus luctus accumsan tortor posuere ac. Morbi quis commodo odio aenean. Accumsan in nisl nisi scelerisque eu. Tincidunt dui ut ornare lectus sit amet est placerat in. Libero enim sed faucibus turpis in eu mi bibendum neque. At lectus urna duis convallis convallis.
Vel risus commodo viverra maecenas accumsan lacus. Mauris pharetra et ultrices neque ornare aenean euismod elementum. Non enim praesent elementum facilisis leo. Amet massa vitae tortor condimentum lacinia. Ornare aenean euismod elementum nisi quis eleifend. Diam donec adipiscing tristique risus nec. Volutpat diam ut venenatis tellus. Mauris nunc congue nisi vitae. Sit amet nisl suscipit adipiscing bibendum est ultricies integer quis. Turpis massa tincidunt dui ut ornare lectus sit amet. Libero enim sed faucibus turpis in. Sit amet porttitor eget dolor morbi non arcu risus quis. Sem integer vitae justo eget magna fermentum iaculis eu. Mattis molestie a iaculis at. Amet volutpat consequat mauris nunc congue. Et tortor at risus viverra adipiscing at in tellus integer. Amet mattis vulputate enim nulla.
Dignissim convallis aenean et tortor. Vitae congue eu consequat ac felis donec et odio. Risus at ultrices mi tempus imperdiet. Amet massa vitae tortor condimentum lacinia quis. Consectetur adipiscing elit ut aliquam purus. Integer quis auctor elit sed vulputate mi sit amet. Tellus id interdum velit laoreet. Sed risus ultricies tristique nulla aliquet. Fermentum dui faucibus in ornare quam. Lobortis elementum nibh tellus molestie nunc non blandit. Amet dictum sit amet justo donec. Iaculis urna id volutpat lacus.""", True)
with open("test.mp4", "wb") as f:
f.write(v)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.
-83
View File
@@ -1,83 +0,0 @@
<!DOCTYPE html><html><head>
<meta property="og:title" content="Watch YouTube on Quest VRChat!!!" />
<meta property="og:description" content="Just type in https://u2b.cx/ followed by whatever you wanna watch!" />
<meta property="og:image" content="https://www.u2b.cx/promo.gif" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="keywords" content="youtube,quest,vrchat" />
<title>Watch YouTube on Quest VRChat with u2b.cx!!!</title>
<style>
img {
vertical-align: middle;
}
code {
white-space: nowrap;
color: brown;
}
</style>
</head><body>
<h2 id="Problem">Problem:</h2>
<img src="VRChat_2023-03-21_22-37-32.758_1920x1080.png" alt="A video player in VRChat" style="max-height: 50vh" />
<p>No video! No PC to put video! No URLs memorized! What do???</p>
<h2 id="Solution">SOLUTION!</h2>
<ol>
<li>Type the following exactly into the URL bar: <code>https://u2b.cx/</code></li>
<li>TIP: Press the copy <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADUAAAA0CAMAAAAdZIDnAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADYUExURRwiKxwqMhsvNhwvNhwuNhwmLxwkLRo9QxhKUBdQVhZTWRZSWBdOVBhGTBsyORlCSRVVXBAdIw8XHQ8VGxNUWRdPVRs0Oxs3PhZUWhJFShlESxJJTxwkLBhJTxArMRVYXho6QBAqLxo6QRhLURApLxVZXxo7Qf///8rKy/39/VZYWfv7+2ZnaFBRU2RlZhRaYBo7QhhIThAxNhVXXRlARxRUWw8VHBZRWBswOBE2PRRYXxlCSBJKUBFITRNKURZTWxlFSxwpMRwtNRo8QxlGTBlFTBo4PxwjLAQa4UkAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADhSURBVEhL7dbXDoIwFIDhulFxD3Di3nvvPd7/jSxyIhjBHJF4I99VT5M/KU1ISr5gsdpw7A4oiNPFuD1eJNbnl6pAMBSOoEVjcTHieJiREox4ymQKRiyenjGdycKEJeToXTB5mLCEAq2KJZiwyhWzkv1ZVX2AjWdaVQ0W6oyu4IRUHTZlb6oGrJuv32ZWSoZWrbaeivppJYMdmXbVgUTtZ9Gu3jErJbNS0ld1e/S90R/AhDXk6NtmNIYJaTKd0crKzmHGWSzF1xfh2MVqjbXZ7vb3ihyOy9MZaXS5StHHCLkBefveLzCsA+EAAAAASUVORK5CYII=" width="24" height="24" /> button on the lower left 😉</li>
<li>Now after that just type what you want to watch. Here are just a few examples:</li>
<ul>
<li><code>https://u2b.cx/nyan cat</code></li>
<li><code>https://u2b.cx/never gonna give you up</code></li>
<li><code>https://u2b.cx/arabic cat</code></li>
<li><code>https://u2b.cx/gangnam style</code></li>
<li><code>https://u2b.cx/miku</code></li>
<li><code>https://u2b.cx/despacito</code></li>
<li><code>https://u2b.cx/baby shark dance</code></li>
<li><code>https://u2b.cx/technology connections</code></li>
</ul>
<li>Just like with the Discord music bots, the first search result from YouTube will start playing.</li>
<li>TIP: Next time, press the paste <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADUAAAA0CAMAAAAdZIDnAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADeUExURRwiKxwqMhsvNxwvNhwtNRsyORhGTBdOVBZRVhZSWBZRVxhLURo+RBwkLBs2PBdPVRNUWg8ZHw8VGw8WHBAdIxVXXBlFSxwlLhhKUBJHTA8WGxZUWho5QBszOhJKTxo6QRVYXhArMBhJTxwkLRo7QRVZXxAqLxo7QhApL6Skpf///1ZXWVxdXvf39/Hx8bKzs7KysrGxsrCxsUlKTBVZXhs4PhVYXREwNhwjLBsvNhRUWxlBRxlBSBE2PRsxOBwoMBZTWhNMUhFITRJHThRYXxdMUhs3PhlFTBo9RBwuNtYqywoAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADwSURBVEhL7dbZboJQEIDhcalacatWRLHWvfUUF1AsLlXrUvX9X6gcnFhCghliYlLDf3OYSb6Qk3ABXFEgGCIWRgHwEInGHonFhUTSQql05ilLLfecFzkqSEXc0CrJZVO9yDhSq/CXvVZxolarNwCaLZyotQVTvXlV776y9c9VhzF8cuSqPhRFYV3GzAM3ttwUs8r2Toczd4XQJDdQ53x1v6rfHwxUVdU0ja6G+D1Z4e4vN3U5X9nzlb1r1AgnapbSxzhR+5QMgEk8hyOx6Yz/SM29XewrveBqKa6+19Q2290PRwCGPovsiYmH4wl5DuAXf0nZllJEIHwAAAAASUVORK5CYII=" width="24" height="24" /> button on the lower right so you don't have to type the whole thing all over again 👍</li>
</ol>
<h3 id="Demo">Demo</h3>
<video controls style="max-height: 80vh"><source src="com.vrchat.oculus.quest-20230321-223838.mp4" type="video/mp4"/></video>
<h3 id="Advanced_usage">Advanced Usage</h3>
<p>For PC users or world creators who want to make a particular video work on Quest, u2b.cx can also be used as an alternative to sites like <a href="https://nextnex.com" target="_blank">nextnex.com</a> and <a href="https://ytdlh.cf/" target="_blank">ytdlh.cf</a> by using the following syntax: <code>https://u2b.cx/id/&lt;youtube video id&gt;</code></p>
<p>Paste a YouTube URL here to have it converted for you: <input id="input" type="text" placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ" style="width: 309px" /> <span id="output_container" style="display: none"><code><span id="output"></span></code> <button id="copy_btn">Copy</button></span></p>
<script>
var input = document.getElementById("input");
var output = document.getElementById("output");
var output_container = document.getElementById("output_container");
var copy_btn = document.getElementById("copy_btn");
input.oninput = function() {
var match = input.value.match(/[A-Za-z0-9_-]{11}/);
if (match) {
output.innerText = `https://u2b.cx/id/${match[0]}`;
output_container.style.display = '';
} else {
output.innerText = '';
output_container.style.display = "none";
}
};
copy_btn.onclick = function() {
navigator.clipboard.writeText(output.innerText);
copy_btn.disabled = true;
copy_btn.innerText = "Copied";
setTimeout(function(){
copy_btn.disabled = false;
copy_btn.innerText = "Copy";
}, 1000);
}
</script>
<p>For potential support of prefixing implementations, known YouTube URL formats will also be parsed for the ID. This means any one of the following will bypass the YouTube search and look up the video directly:</p>
<ul>
<li><code>https://u2b.cx/id/dQw4w9WgXcQ</code> (recommended)</li>
<li><code>https://u2b.cx/https://www.youtube.com/watch?v=dQw4w9WgXcQ</code></li>
<li><code>https://u2b.cx/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ</code></li>
<li><code>https://u2b.cx/http://youtube.com/watch?v=dQw4w9WgXcQ</code></li>
<li><code>https://u2b.cx/youtube.com/watch?v=dQw4w9WgXcQ</code></li>
<li><code>https://u2b.cx/https://youtu.be/dQw4w9WgXcQ</code></li>
<li><code>https://u2b.cx/https://youtube/shorts/dQw4w9WgXcQ</code></li>
</ul>
<p>Anything else will be sent to YouTube search, which will probably still get what you want.</p>
<h3 id="Problems">Problems?</h3>
<p>If you encounter any issues such as a service outage, please submit to the <a href="https://gitea.moe/lamp/u2b.cx/issues/new">issue tracker</a>. Thanks.</p>
</body></html>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB