Compare commits
13 Commits
879661a2ac
...
nginx
| Author | SHA1 | Date | |
|---|---|---|---|
| 669ea6a41b | |||
| b2a3c5a048 | |||
| 5a91e654f8 | |||
| 3a19f8fe72 | |||
| b49918f8a6 | |||
| a79ee967d2 | |||
| cddbbcc866 | |||
| a007796147 | |||
| 83690c4535 | |||
| 8057cdb44d | |||
| 8364c23c5f | |||
| 510cca1b3a | |||
| 14e5fe6df7 |
@@ -1,36 +0,0 @@
|
|||||||
#edit for deployment
|
|
||||||
#u2b.cx {
|
|
||||||
:80 {
|
|
||||||
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}
|
|
||||||
transport http {
|
|
||||||
tls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handle {
|
|
||||||
respond 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
-4
@@ -2,8 +2,8 @@ FROM python:3.11
|
|||||||
RUN useradd -r -m u2b
|
RUN useradd -r -m u2b
|
||||||
RUN apt update && apt install -y ffmpeg
|
RUN apt update && apt install -y ffmpeg
|
||||||
RUN pip install --no-cache-dir python-ffmpeg yt-dlp
|
RUN pip install --no-cache-dir python-ffmpeg yt-dlp
|
||||||
COPY . /app
|
#COPY . /app
|
||||||
WORKDIR /app
|
#WORKDIR /app
|
||||||
USER u2b
|
USER u2b
|
||||||
ENV PORT=8080
|
#ENV PORT=8080
|
||||||
CMD ["python", "server.py"]
|
#CMD ["python", "server.py"]
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
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 {
|
||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+20
-6
@@ -4,19 +4,33 @@ services:
|
|||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./:/app/
|
||||||
|
working_dir: /app/
|
||||||
environment:
|
environment:
|
||||||
|
- PORT=8080
|
||||||
- PROXY=/proxy/
|
- PROXY=/proxy/
|
||||||
caddy:
|
command: python server.py
|
||||||
image: caddy:2.6
|
# 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
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "443:443/udp"
|
- "443:443/udp"
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
volumes:
|
volumes:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
caddy_config:
|
caddy_config:
|
||||||
+56
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,33 +11,24 @@ 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 = []
|
||||||
|
|
||||||
def cache_prune_loop():
|
def cache_prune_loop():
|
||||||
while True:
|
while True:
|
||||||
sleep(3600)
|
sleep(3600)
|
||||||
for query, ctx in ctx_cache:
|
for key in ctx_cache:
|
||||||
if datetime.now() >= ctx['expire']:
|
if datetime.now() >= ctx_cache[key]['expire']:
|
||||||
del ctx[query]
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user