Compare commits
9 Commits
8057cdb44d
...
669ea6a41b
Author | SHA1 | Date | |
---|---|---|---|
669ea6a41b | |||
b2a3c5a048 | |||
5a91e654f8 | |||
3a19f8fe72 | |||
b49918f8a6 | |||
a79ee967d2 | |||
cddbbcc866 | |||
a007796147 | |||
83690c4535 |
26
README.md
26
README.md
@ -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,7 +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 video
|
- Errors displayed as a 10 second single-frame video
|
||||||
|
- PC VRchat bypassed to save bandwidth (todo: sacrifices consistency)
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
@ -21,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.
|
@ -1,6 +1,21 @@
|
|||||||
#edit for deployment
|
{
|
||||||
#u2b.cx {
|
log http.log.access {
|
||||||
|
include http.log.access
|
||||||
|
output stdout
|
||||||
|
format formatted "[35m[{ts}][0m [96m[1m{request>remote_ip}[0m [31m{request>headers>X-Forwarded-For}[0m [33m{request>method}[0m [92m{request>host}[32m{request>uri}[0m [97m{status}[0m [90m{request>headers>User-Agent}[0m [34m{request>headers>Referer}[0m" {
|
||||||
|
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 {
|
4
caddy/Dockerfile
Normal file
4
caddy/Dockerfile
Normal 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
|
38
server.py
38
server.py
@ -11,15 +11,6 @@ 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 = []
|
||||||
|
|
||||||
@ -29,15 +20,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).start()
|
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):
|
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=""):
|
||||||
@ -55,8 +46,16 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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:]
|
||||||
|
|
||||||
ctx = ctx_cache.get(query)
|
ctx = ctx_cache.get(query)
|
||||||
|
|
||||||
@ -67,7 +66,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
ips_running_ytdl.append(client_ip)
|
ips_running_ytdl.append(client_ip)
|
||||||
ctx_cache[query] = ctx = {'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)
|
||||||
|
|
||||||
@ -86,7 +88,11 @@ 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
|
||||||
|
Loading…
Reference in New Issue
Block a user