AI slop update to switch warp docker ip on bot block

This commit is contained in:
jenz
2026-08-24 14:47:44 +02:00
parent 9c4c02c532
commit 0346c0667b
4 changed files with 247 additions and 28 deletions
@@ -8,6 +8,58 @@ import math
from .Utils import Utils, DataHolder
import traceback
# Path to the WARP IP-rotation script. Must be executable (chmod +x) and live
# in the same folder as _start.sh (the systemd unit's WorkingDirectory), since
# rotate_warp.sh itself cd's into the warp-docker compose folder.
WARP_ROTATE_SCRIPT = "/home/gameservers/css_ze/torchlight3/rotate_warp.sh"
WARP_ROTATE_TIMEOUT = 60 # seconds to allow rotate_warp.sh to bring WARP back up
# yt-dlp's stderr when YouTube's bot-check trips. Matched as two substrings
# rather than one exact string since the apostrophe yt-dlp prints can vary
# (straight vs curly quote) between versions.
YOUTUBE_BOT_CHECK_MARKERS = (b"Sign in to confirm", b"not a bot")
async def run_ytdlp_with_rotation(logger, *args):
"""
Run `yt-dlp *args` exactly as before. If yt-dlp's stderr shows YouTube's
"Sign in to confirm you're not a bot" bot-check, rotate the WARP proxy's
IP once (via rotate_warp.sh) and transparently retry the exact same
yt-dlp call before giving up. The caller (and the end user) never sees
the first, blocked attempt -- they just get a result, possibly a couple
seconds slower.
Returns (stdout_bytes, stderr_bytes) from the final attempt.
"""
Out, Err = b"", b""
for attempt in (1, 2):
Proc = await asyncio.create_subprocess_exec(
"yt-dlp", *args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
Out, Err = await Proc.communicate()
BotChecked = all(marker in Err for marker in YOUTUBE_BOT_CHECK_MARKERS)
if BotChecked and attempt == 1:
logger.warning("yt-dlp hit YouTube's bot-check, rotating WARP IP and retrying: %s", args)
try:
RotateProc = await asyncio.create_subprocess_exec(
WARP_ROTATE_SCRIPT,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
RotateOut, RotateErr = await asyncio.wait_for(
RotateProc.communicate(), timeout=WARP_ROTATE_TIMEOUT)
if RotateProc.returncode != 0:
logger.error("rotate_warp.sh failed (rc=%s): %s", RotateProc.returncode,
RotateErr.decode(errors="replace"))
except (asyncio.TimeoutError, OSError) as e:
logger.error("WARP rotation did not complete in time, retrying with same IP anyway: %s", e)
continue
break
return Out, Err
def get_birtate(message):
bitrate = []
try:
@@ -114,18 +166,22 @@ class URLFilter(BaseCommand):
if TimeStr:
Time = Utils.ParseTime(TimeStr)
Proc = await asyncio.create_subprocess_exec("yt-dlp", "--dump-json", "-g", url,
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
# No "-g" here: we only need metadata (title/duration/webpage_url)
# for the chat message. Actual playback happens by handing the
# canonical webpage URL to yt-dlp again at play time (see
# YTDLP_URI_PREFIX in FFmpegAudioPlayer.py) rather than resolving
# to a direct googlevideo URL here and having a separate process
# (ffmpeg) try to fetch it -- that signed-URL handoff turned out
# to reliably 403 even with matching proxy/headers.
Out, Err = await run_ytdlp_with_rotation(self.Logger, "--dump-json", "--no-playlist", url)
parts = Out.split(b'\n')
parts.pop() # trailing new line
if not Out.strip():
self.Logger.error("yt-dlp returned no output for %s: %s", url, Err.decode(errors = "replace"))
self.Torchlight().SayChat("Error: Could not fetch YouTube info, try again in a moment.")
return url, Text
Info = parts.pop()
url = parts.pop()
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
Info = self.json.loads(Out.strip().split(b'\n')[-1])
url = "ytdlp:" + Info.get("webpage_url", url)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2:,}".format(
@@ -704,14 +760,19 @@ class YouTubeSearch(BaseCommand):
message[1] = message[1][:Temp.value]
search_term = message[1].split("pitch=")[0].split("tempo=")[0].split('backward=')[0].split('backwards=')[0]
Proc = await asyncio.create_subprocess_exec("yt-dlp", "--dump-json", "-xg", "ytsearch:" + search_term,
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
# No "-xg" here: only metadata is needed at this point. Playback goes
# through yt-dlp itself again (piped into ffmpeg) rather than a
# resolved googlevideo URL -- see the comment above YTDLP_URI_PREFIX
# in FFmpegAudioPlayer.py for why.
Out, Err = await run_ytdlp_with_rotation(self.Logger, "--dump-json", "--no-playlist", "ytsearch:" + search_term)
print('out value: ', Out)
url, Info = Out.split(b'\n', maxsplit = 1)
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
if not Out.strip():
self.Logger.error("yt-dlp returned no output for search '%s': %s", search_term, Err.decode(errors = "replace"))
self.Torchlight().SayChat("Error: Could not fetch YouTube info, try again in a moment.")
return 1
Info = self.json.loads(Out.strip().split(b'\n')[-1])
url = "ytdlp:" + Info.get("webpage_url", "ytsearch:" + search_term)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2:,}".format(
@@ -987,4 +1048,3 @@ class Reload(BaseCommand):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().Reload()
return 0