Compare commits
15 Commits
f9fab223e8
...
nginx
| Author | SHA1 | Date | |
|---|---|---|---|
| 669ea6a41b | |||
| b2a3c5a048 | |||
| 5a91e654f8 | |||
| 3a19f8fe72 | |||
| b49918f8a6 | |||
| a79ee967d2 | |||
| cddbbcc866 | |||
| a007796147 | |||
| 83690c4535 | |||
| 8057cdb44d | |||
| 8364c23c5f | |||
| 510cca1b3a | |||
| 14e5fe6df7 | |||
| 879661a2ac | |||
| c5e2eb3c37 |
@@ -1,32 +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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -1,8 +1,12 @@
|
||||
# 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
|
||||
- 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
|
||||
- Results cached for 5 hours or until expiry found in extracted URL
|
||||
- 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
|
||||
|
||||
@@ -21,8 +26,19 @@ A YouTube video file resolver thingy via search query, made to let you watch You
|
||||
|
||||
### 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/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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,32 +3,32 @@ 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 time import sleep
|
||||
from os import environ
|
||||
import logging
|
||||
import re
|
||||
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 = {}
|
||||
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):
|
||||
def address_string(self):
|
||||
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')
|
||||
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=""):
|
||||
@@ -46,8 +46,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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:]
|
||||
match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.|music\.|m\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", path)
|
||||
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)
|
||||
|
||||
@@ -58,7 +66,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
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:
|
||||
info = ydl.extract_info(query, download=False)
|
||||
|
||||
@@ -77,7 +88,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
best_format = max(suitable_formats, key=lambda x: x['height'])
|
||||
|
||||
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:
|
||||
logging.exception(e)
|
||||
ctx['exception'] = e
|
||||
|
||||
Reference in New Issue
Block a user