Compare commits

..

36 Commits

Author SHA1 Message Date
lamp 9de3ef8e1f restart always 2023-12-05 23:14:48 -06:00
lamp 92ad71dbd4 python3.11 2023-11-27 19:11:00 -06:00
lamp bd3e1447e4 update regex 2023-09-19 02:59:21 -07:00
lamp eec86be88c disable resolver 2023-09-19 02:28:47 -07:00
lamp a2b64041ba fix cache prune 2023-08-19 14:37:46 -07:00
lamp 162bd1c1b9 fix using expired 2023-08-19 14:03:24 -07:00
lamp 63cc3e90b9 fix formatting 2023-08-18 20:03:42 -05:00
lamp a08303eb3a [REFACTOR] allow selecting Nth search result 2023-08-18 17:58:31 -07:00
lamp 6bc155c6d2 delete commented 2023-08-17 23:45:33 -07:00
lamp f704098473 use host network mode 2023-08-15 19:19:47 -07:00
lamp d5ead66a07 rename env 2023-08-15 11:35:48 -07:00
lamp f8436927cf use env for caddy site 2023-08-15 11:34:02 -07:00
lamp 3b4ee632b1 why do I have log template but then don't log 2023-08-15 11:29:30 -07:00
lamp 6b868c2b63 Disable PC vrchat bypass 2023-08-15 11:26:52 -07:00
lamp 40bda207e1 skip streaming manifests 2023-08-01 00:35:51 -07:00
lamp ea8bf8fa1e porkbun dns 2023-07-31 21:34:04 -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 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
16 changed files with 305 additions and 214 deletions
+4
View File
@@ -1 +1,5 @@
.vscode
__pycache__
test.mp4
env
errors
-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"]
-27
View File
@@ -1,27 +0,0 @@
# 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.
## Features
- Written in Python to integrate with YoutubeDL (yt-dlp) for fastest performance
- Multi-threaded for concurrent usage
- Requests coalesced to one YoutubeDL invocation per input
- Limited to one YoutubeDL invocation per IP address
- Results cached for 5 hours or until expiry found in extracted URL
- Extracted URLs proxied in Caddy so that they work in all countries
### Planned
- Option to get Nth search result (requires deeper integration into YoutubeDL)
## Usage
### 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.
### GET `https://u2b.cx/id/<video id>`
Bypasses search to serve the video directly.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

+56
View File
@@ -0,0 +1,56 @@
{
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
}
}
{$CADDY_SITE:":80"} {
log
tls {
dns porkbun {
api_key {env.PORKBUN_API_KEY}
api_secret_key {env.PORKBUN_API_SECRET}
}
}
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://127.0.0.1:8080
}
}
}
+6
View File
@@ -0,0 +1,6 @@
FROM caddy:2.6-builder AS builder
RUN xcaddy build \
--with github.com/caddyserver/transform-encoder \
--with github.com/caddy-dns/porkbun
FROM caddy:2.6
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
+27
View File
@@ -0,0 +1,27 @@
version: "3.8"
name: "u2bcx"
services:
app:
build: .
restart: always
volumes:
- ./:/app/
working_dir: /app/
environment:
- ADDRESS=127.0.0.1
- PORT=8080
- PROXY=/proxy/
network_mode: host
command: python server.py
caddy:
build: caddy
restart: always
network_mode: host
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
env_file: env
volumes:
caddy_data:
caddy_config:
BIN
View File
Binary file not shown.
+195 -60
View File
@@ -1,82 +1,217 @@
from yt_dlp import YoutubeDL
from importlib.metadata import version
print("yt-dlp version", version("yt_dlp"))
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse, parse_qs
from threading import Event
from threading import Event, Thread
from datetime import datetime, timedelta
from os import environ
from time import sleep
from os import environ, makedirs, stat
import logging
import re
from ffmpeg import FFmpeg
import textwrap
from pathlib import Path
from hashlib import sha256
from shutil import copyfileobj
from math import ceil
def getBestMp4UrlFromInfo(info):
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 = []
if 'DEBUG' in environ:
logging.basicConfig(level=logging.DEBUG)
class Ratelimit(Exception): pass
class CachedException(Exception): pass
class Handler(BaseHTTPRequestHandler):
def address_string(self):
return getattr(self, 'headers', {}).get('X-Forwarded-For', '').split(',')[0] or self.client_address[0]
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 send_error_video(self, text: str):
makedirs("errors", exist_ok=True)
hash = sha256(bytes(text, "utf8")).hexdigest()
file = Path(f"errors/{hash}.mp4")
if not file.exists():
text = re.sub("(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]", '', text)
text = text.replace("\\", "\\\\").replace('"', '""').replace("'", "''").replace("%", "\\%").replace(":", "\\:")
text = textwrap.fill(text, 90)
peg = FFmpeg().option("y").input("bg.png", {
"framerate": "0.1"
}).output(str(file), {
"f": "mp4",
"t": "10",
"c:v": "libx264",
"pix_fmt": "yuv420p",
"vf": "drawtext=font=monospace:fontsize=24:x=10:y=10:text='"+text+"':"
})
@peg.on("start")
def on_start(arguments):
logging.debug("cmd:" + ' '.join(arguments))
@peg.on("stderr")
def on_stderr(line):
logging.debug("stderr:" + line)
peg.execute()
with file.open('rb') as f:
fs = stat(f.fileno())
self.send_response(200)
self.send_header("Content-Type", "video/mp4")
self.send_header("Content-Length", str(fs.st_size))
self.end_headers()
copyfileobj(f, self.wfile)
def do_GET(self):
# block other bot junk in reverse proxy
if self.path in ["/", "/favicon.ico"]:
self.send_error(404)
try:
if self.path in ["/", "/favicon.ico"] or self.path.startswith("/."):
self.send_error(404)
return
path = unquote(self.path)
id_match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.|music\.|m\.)?youtube\.com\/(?:watch\?v=|shorts\/|live\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path)
if id_match:
video_id = id_match[1]
else:
search_match = re.match("^\/(.+?)(?:\/(\d*))?$", path)
if not search_match:
self.send_error(404)
return
search_query = search_match[1]
search_index = int(search_match[2]) if search_match[2] and search_match[2].isdigit() else 1
search_index = max(min(search_index, 100), 1)
video_id = ytdl_search_to_id(self, search_query, search_index)
self.send_response(302)
self.send_header("Location", f"https://www.youtube.com/watch?v={video_id}")
self.end_headers()
#half this code now defunct
return
path = unquote(self.path)
match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path)
query = match[1] if match else "ytsearch:" + path[1:]
video_url = ytdl_resolve_mp4_url(self, video_id)
dict = query_dicts.get(query)
if not dict or 'expire' in dict and datetime.now() >= dict['expire']:
client_ip = self.address_string()
if client_ip in ips_running_ytdl:
self.send_error(429)
return
try:
ips_running_ytdl.append(client_ip)
query_dicts[query] = dict = {'event': Event()}
with YoutubeDL() as ydl:
info = ydl.extract_info(query, download=False)
url = getBestMp4UrlFromInfo(info)
dict['url'] = url
dict['expire'] = getExpireDatetime(url)
except Exception as e:
logging.exception(e)
dict['exception'] = e
finally:
ips_running_ytdl.remove(client_ip)
dict['event'].set()
elif 'url' not in dict:
dict['event'].wait(60)
if not dict.get('url'):
if 'exception' in dict:
self.send_error(500, message=str(dict['exception']), explain="An unexpected exception was encountered while resolving this query")
else:
self.send_error(404)
else:
url = dict['url']
if 'PROXY' in environ:
url = environ['PROXY'] + url.replace("https://",'')
video_url = environ['PROXY'] + video_url.replace("https://",'')
self.send_response(302)
self.send_header("Location", url)
self.send_header("Location", video_url)
self.end_headers()
except Ratelimit:
self.send_error(429)
except CachedException as e:
self.send_error_video(str(e))
except Exception as e:
logging.exception(e)
self.send_error_video(str(e))
ips_running_ytdl = []
def invoke_youtubedl(self: Handler, input: str) -> dict:
ip = self.address_string()
if ip in ips_running_ytdl:
raise Ratelimit()
ips_running_ytdl.append(ip)
try:
with YoutubeDL({'extractor_args': {'youtube': {'skip': ['dash', 'hls']}}}) as ydl:
return ydl.extract_info(input, download=False, process=False)
finally:
ips_running_ytdl.remove(ip)
search_cache = {}
def ytdl_search_to_id(self: Handler, query: str, index: int) -> str:
ctx = search_cache.get(query)
if ctx:
ctx['event'].wait(60)
if 'error' in ctx:
raise CachedException(ctx['error'])
results = ctx.get('results')
else:
results = None
if results == None or results['count'] < index or datetime.now() >= ctx['expires_at']:
search_cache[query] = ctx = {
'event': Event(),
'expires_at': datetime.now() + timedelta(hours=5)
}
try:
count = ceil(index/10)*10
info = invoke_youtubedl(self, f"ytsearch{count}:{query}")
entries = list(info['entries'])
if not entries:
raise Exception("ERROR: No results!")
ids = [video['id'] for video in entries]
ctx['results'] = results = {'ids': ids, 'count': count}
except Exception as e:
ctx['error'] = str(e)
raise
finally:
ctx['event'].set()
return results['ids'][min(index, len(results['ids'])) - 1]
resolve_cache = {}
def ytdl_resolve_mp4_url(self: Handler, input: str) -> str:
ctx = resolve_cache.get(input)
if ctx and datetime.now() <= ctx['expires_at']:
ctx['event'].wait(60)
if 'error' in ctx:
raise CachedException(ctx['error'])
return ctx['url']
resolve_cache[input] = ctx = {
'event': Event(),
'expires_at': datetime.now() + timedelta(hours=5)
}
try:
info = invoke_youtubedl(self, input)
selection = info
if "entries" in info:
if not info["entries"]:
raise Exception("ERROR: No video found!")
else:
selection = info["entries"][0]
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'] = url = best_format['url']
try:
expire = parse_qs(urlparse(url).query).get('expire', [])[0]
if expire:
expire = datetime.fromtimestamp(int(expire))
if expire < ctx['expires_at']:
ctx['expires_at'] = expire
except Exception as e:
logging.exception("failed parsing expire", e)
except Exception as e:
ctx['error'] = str(e)
raise
finally:
ctx['event'].set()
return url
def cache_prune_loop():
while True:
sleep(3600)
for key in list(search_cache.keys()):
if datetime.now() >= search_cache[key]['expires_at']:
del search_cache[key]
for key in list(resolve_cache.keys()):
if datetime.now() >= resolve_cache[key]['expires_at']:
del resolve_cache[key]
Thread(target=cache_prune_loop, daemon=True).start()
with ThreadingHTTPServer((environ.get('ADDRESS', ''), int(environ.get('PORT', 80))), Handler) as server:
server.serve_forever()
+4 -2
View File
@@ -7,9 +7,11 @@ After=network.target
User=u2b
Group=u2b
WorkingDirectory=/srv/u2b.cx/
Environment=ADDRESS=127.29.151.200 PORT=52482 PROXY=https://proxy.u2b.cx/
ExecStart=/usr/bin/python3.9 server.py
Environment=ADDRESS=127.0.0.1 PORT=52482
ExecStart=/usr/bin/python3.11 server.py
MemoryMax=1G
LimitNOFILE=262144
Restart=always
[Install]
WantedBy=multi-user.target
Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 B

-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