Compare commits
7 Commits
a08303eb3a
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9de3ef8e1f | |||
| 92ad71dbd4 | |||
| bd3e1447e4 | |||
| eec86be88c | |||
| a2b64041ba | |||
| 162bd1c1b9 | |||
| 63cc3e90b9 |
@@ -1,45 +0,0 @@
|
|||||||
# u2b.cx
|
|
||||||
|
|
||||||
A YouTube search resolver + raw file resolver w/ proxy for Quest VRChat.
|
|
||||||
|
|
||||||
- Get the video you want just by typing its name in the URL
|
|
||||||
- Video works for both PC and Quest VRChat
|
|
||||||
- Proxying avoids unpredictable blocks from google's servers
|
|
||||||
|
|
||||||
## Technical 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
|
|
||||||
- Errors displayed as a 10 second single-frame video
|
|
||||||
|
|
||||||
## Potential improvements
|
|
||||||
|
|
||||||
- Support other sites besides YouTube
|
|
||||||
- Rewrite on Twisted framework
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### GET `https://u2b.cx/<query>(/<number>)`
|
|
||||||
|
|
||||||
Get a YouTube video via search query. Optional <number> parameter selects the Nth result, default is first result. Max 100. Query may not start with `.`.
|
|
||||||
|
|
||||||
Example: https://u2b.cx/nyan%20cat%20cover/2 will play the second result from the search results of "nyan cat cover".
|
|
||||||
|
|
||||||
Responds with 302 redirect to raw mp4 file, or 200 with an error video if something went wrong.
|
|
||||||
|
|
||||||
### 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... (see regex in code)
|
|
||||||
|
|
||||||
Bypasses search to resolve the video directly by its id.
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -74,7 +74,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
path = unquote(self.path)
|
path = unquote(self.path)
|
||||||
id_match = re.match("\/(?:id\/|(?:https?:\/\/)?(?:(?:www\.|music\.|m\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/))([A-Za-z0-9_-]{11})", 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:
|
if id_match:
|
||||||
video_id = id_match[1]
|
video_id = id_match[1]
|
||||||
@@ -88,6 +88,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
search_index = max(min(search_index, 100), 1)
|
search_index = max(min(search_index, 100), 1)
|
||||||
video_id = ytdl_search_to_id(self, search_query, search_index)
|
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
|
||||||
|
|
||||||
video_url = ytdl_resolve_mp4_url(self, video_id)
|
video_url = ytdl_resolve_mp4_url(self, video_id)
|
||||||
|
|
||||||
if 'PROXY' in environ:
|
if 'PROXY' in environ:
|
||||||
@@ -128,7 +134,7 @@ def ytdl_search_to_id(self: Handler, query: str, index: int) -> str:
|
|||||||
else:
|
else:
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
if results == None or results['count'] < index:
|
if results == None or results['count'] < index or datetime.now() >= ctx['expires_at']:
|
||||||
search_cache[query] = ctx = {
|
search_cache[query] = ctx = {
|
||||||
'event': Event(),
|
'event': Event(),
|
||||||
'expires_at': datetime.now() + timedelta(hours=5)
|
'expires_at': datetime.now() + timedelta(hours=5)
|
||||||
@@ -153,7 +159,7 @@ def ytdl_search_to_id(self: Handler, query: str, index: int) -> str:
|
|||||||
resolve_cache = {}
|
resolve_cache = {}
|
||||||
def ytdl_resolve_mp4_url(self: Handler, input: str) -> str:
|
def ytdl_resolve_mp4_url(self: Handler, input: str) -> str:
|
||||||
ctx = resolve_cache.get(input)
|
ctx = resolve_cache.get(input)
|
||||||
if ctx:
|
if ctx and datetime.now() <= ctx['expires_at']:
|
||||||
ctx['event'].wait(60)
|
ctx['event'].wait(60)
|
||||||
if 'error' in ctx:
|
if 'error' in ctx:
|
||||||
raise CachedException(ctx['error'])
|
raise CachedException(ctx['error'])
|
||||||
@@ -199,10 +205,10 @@ def ytdl_resolve_mp4_url(self: Handler, input: str) -> str:
|
|||||||
def cache_prune_loop():
|
def cache_prune_loop():
|
||||||
while True:
|
while True:
|
||||||
sleep(3600)
|
sleep(3600)
|
||||||
for key in search_cache:
|
for key in list(search_cache.keys()):
|
||||||
if datetime.now() >= search_cache[key]['expires_at']:
|
if datetime.now() >= search_cache[key]['expires_at']:
|
||||||
del search_cache[key]
|
del search_cache[key]
|
||||||
for key in resolve_cache:
|
for key in list(resolve_cache.keys()):
|
||||||
if datetime.now() >= resolve_cache[key]['expires_at']:
|
if datetime.now() >= resolve_cache[key]['expires_at']:
|
||||||
del resolve_cache[key]
|
del resolve_cache[key]
|
||||||
Thread(target=cache_prune_loop, daemon=True).start()
|
Thread(target=cache_prune_loop, daemon=True).start()
|
||||||
|
|||||||
+4
-2
@@ -7,9 +7,11 @@ After=network.target
|
|||||||
User=u2b
|
User=u2b
|
||||||
Group=u2b
|
Group=u2b
|
||||||
WorkingDirectory=/srv/u2b.cx/
|
WorkingDirectory=/srv/u2b.cx/
|
||||||
Environment=ADDRESS=127.29.151.200 PORT=52482 PROXY=https://proxy.u2b.cx/
|
Environment=ADDRESS=127.0.0.1 PORT=52482
|
||||||
ExecStart=/usr/bin/python3.9 server.py
|
ExecStart=/usr/bin/python3.11 server.py
|
||||||
MemoryMax=1G
|
MemoryMax=1G
|
||||||
|
LimitNOFILE=262144
|
||||||
|
Restart=always
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
Reference in New Issue
Block a user