Compare commits

..

No commits in common. "669ea6a41b85d82b576cea8fe2bf75e0f04b56ed" and "8057cdb44df54902e9780a90c66791e6b047c1d6" have entirely different histories.

4 changed files with 23 additions and 64 deletions

View File

@ -1,21 +1,6 @@
{ #edit for deployment
log http.log.access { #u2b.cx {
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 { :80 {
#nl.u2b.cx u2b.cx {
log
handle_path /proxy/* { handle_path /proxy/* {
@gv path_regexp gvurl ^\/([a-z0-9-]+\.googlevideo\.com) @gv path_regexp gvurl ^\/([a-z0-9-]+\.googlevideo\.com)
handle @gv { handle @gv {

View File

@ -1,12 +1,8 @@
# u2b.cx # u2b.cx
A YouTube search resolver + raw file resolver w/ proxy for Quest VRChat. 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.
- Get the video you want just by typing its name in the URL ## Features
- 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
@ -14,8 +10,7 @@ A YouTube search resolver + raw file resolver w/ proxy for Quest VRChat.
- 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 - Errors displayed as a video
- PC VRchat bypassed to save bandwidth (todo: sacrifices consistency)
### Planned ### Planned
@ -26,19 +21,8 @@ A YouTube search resolver + raw file resolver w/ proxy for Quest VRChat.
### 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. If the client is PC VRChat, the server may instead redirect to the YouTube video URL to save bandwidth on the server. 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.
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 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. Bypasses search to serve the video directly.
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.

View File

@ -1,4 +0,0 @@
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

View File

@ -11,6 +11,15 @@ import logging
import re import re
from textvid import generate_video_from_text from textvid import generate_video_from_text
def get_expire(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])
ctx_cache = {} ctx_cache = {}
ips_running_ytdl = [] ips_running_ytdl = []
@ -20,15 +29,15 @@ def cache_prune_loop():
for key in ctx_cache: for key in ctx_cache:
if datetime.now() >= ctx_cache[key]['expire']: if datetime.now() >= ctx_cache[key]['expire']:
del ctx_cache[key] del ctx_cache[key]
Thread(target=cache_prune_loop, daemon=True).start() Thread(target=cache_prune_loop).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): def is_pc_vrchat(self):
ua = self.headers.get('User-Agent', '') ua = self.headers.get('User-Agent')
ae = self.headers.get('Accept-Encoding', '') 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" 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=""): def send_error(self, code, message=""):
@ -46,16 +55,8 @@ class Handler(BaseHTTPRequestHandler):
return return
path = unquote(self.path) path = unquote(self.path)
match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.|music\.|m\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path) match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path)
if match: query = match[1] if match else "ytsearch:" + path[1:]
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:]
ctx = ctx_cache.get(query) ctx = ctx_cache.get(query)
@ -66,10 +67,7 @@ class Handler(BaseHTTPRequestHandler):
return return
try: try:
ips_running_ytdl.append(client_ip) ips_running_ytdl.append(client_ip)
ctx_cache[query] = ctx = { ctx_cache[query] = ctx = {'event': Event()}
'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)
@ -88,11 +86,7 @@ class Handler(BaseHTTPRequestHandler):
best_format = max(suitable_formats, key=lambda x: x['height']) best_format = max(suitable_formats, key=lambda x: x['height'])
ctx['url'] = best_format['url'] ctx['url'] = best_format['url']
ctx['expire'] = get_expire(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)
ctx['exception'] = e ctx['exception'] = e