From 14d41bd0cdf12cad095dbedf902ae207fb2c8e1b Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:44:51 +0100 Subject: [PATCH] modified: app.py --- app.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index cee815c..5e1ea87 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,8 @@ from typing import Optional, List, Dict from concurrent.futures import ThreadPoolExecutor import functools import traceback +import base64 +from pathlib import Path # Optional: YouTube extraction try: @@ -323,6 +325,52 @@ def get_team_emoji(ctx: commands.Context, team_name: str) -> str: # Global music state per guild MUSIC_STATE: Dict[int, Dict] = {} +# --- yt-dlp cookies and options support --- +# You can supply YouTube cookies to bypass bot checks/age gates: +# - YTDL_COOKIES_FILE: Path to a Netscape-format cookies.txt inside the container (e.g., mounted secret) +# - YTDL_COOKIES_B64: Base64-encoded content of a Netscape-format cookies.txt; will be written to app directory +# - YTDL_COOKIES_FROM_BROWSER: e.g., "chrome:Default" (only works on hosts with a real browser profile, usually not in containers) +# - YTDL_UA: Custom User-Agent string +# - YTDL_YT_CLIENT: youtube player client hint (e.g., android, web) to work around some restrictions (default: android) + +YTDL_COOKIEFILE: Optional[str] = None +YTDL_COOKIESFROMBROWSER: Optional[tuple] = None + +try: + # Priority 1: cookies from base64 env + _cookies_b64 = os.getenv('YTDL_COOKIES_B64') + if _cookies_b64: + try: + decoded = base64.b64decode(_cookies_b64) + cookie_path = Path(__file__).with_name('cookies.txt') + cookie_path.write_bytes(decoded) + YTDL_COOKIEFILE = str(cookie_path) + print(f"🍪 Wrote YouTube cookies to {YTDL_COOKIEFILE} (from YTDL_COOKIES_B64)") + except Exception as e: + print(f"⚠️ Failed to decode/write YTDL_COOKIES_B64: {e}") + + # Priority 2: cookies from a file path + if not YTDL_COOKIEFILE: + _cookies_file = os.getenv('YTDL_COOKIES_FILE') + if _cookies_file and os.path.exists(_cookies_file): + YTDL_COOKIEFILE = _cookies_file + print(f"🍪 Using YouTube cookies file: {YTDL_COOKIEFILE}") + elif _cookies_file: + print(f"⚠️ YTDL_COOKIES_FILE set but not found: {_cookies_file}") + + # Optional: cookies from browser (rarely usable in containers) + _cookies_from_browser = os.getenv('YTDL_COOKIES_FROM_BROWSER') + if _cookies_from_browser: + parts = _cookies_from_browser.split(':') + browser = parts[0].strip().lower() if parts else None + profile = parts[1].strip() if len(parts) > 1 else None + if browser: + # (browser, profile, keyring, container) + YTDL_COOKIESFROMBROWSER = (browser, profile, None, None) + print(f"🍪 Will try cookies from browser: {browser} profile={profile or 'default'}") +except Exception as e: + print(f"⚠️ Cookie configuration error: {e}") + YTDL_OPTS = { 'format': 'bestaudio/best', 'noplaylist': True, @@ -331,6 +379,28 @@ YTDL_OPTS = { 'skip_download': True, } +def get_ytdl_opts() -> Dict: + """Build yt-dlp options dynamically, injecting cookies and headers if configured.""" + opts = dict(YTDL_OPTS) + # UA and extractor tweaks + ua = os.getenv('YTDL_UA') or ( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/118.0 Safari/537.36' + ) + yt_client = os.getenv('YTDL_YT_CLIENT', 'android') + opts['http_headers'] = {'User-Agent': ua} + # Extractor args can help avoid some player config checks + opts.setdefault('extractor_args', {}) + opts['extractor_args'].setdefault('youtube', {}) + # player_client hint (e.g., android) + opts['extractor_args']['youtube']['player_client'] = [yt_client] + # Use cookies if available + if YTDL_COOKIEFILE: + opts['cookiefile'] = YTDL_COOKIEFILE + elif YTDL_COOKIESFROMBROWSER: + opts['cookiesfrombrowser'] = YTDL_COOKIESFROMBROWSER + return opts + FFMPEG_BEFORE = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5" FFMPEG_OPTS = { 'before_options': FFMPEG_BEFORE, @@ -375,7 +445,7 @@ async def _ytdlp_extract(loop: asyncio.AbstractEventLoop, query: str) -> Optiona if not ytdlp: return None def _extract(): - with ytdlp.YoutubeDL(YTDL_OPTS) as ytdl: + with ytdlp.YoutubeDL(get_ytdl_opts()) as ytdl: return ytdl.extract_info(query, download=False) try: info = await loop.run_in_executor(executor, _extract) @@ -384,7 +454,12 @@ async def _ytdlp_extract(loop: asyncio.AbstractEventLoop, query: str) -> Optiona if 'entries' in info: info = info['entries'][0] return info - except Exception: + except Exception as e: + # Make YouTube cookie issues clearer in logs + msg = str(e) + if 'Sign in to confirm you’re not a bot' in msg or 'Use --cookies' in msg or 'pass cookies' in msg: + print("❌ yt-dlp error: YouTube requires cookies to proceed.") + print(" Provide YTDL_COOKIES_FILE or YTDL_COOKIES_B64 (Netscape cookies.txt).") traceback.print_exc() return None @@ -481,7 +556,9 @@ async def play(ctx: commands.Context, *, query: str): await ctx.reply("🔎 Searching…") item = await _create_audio_source(loop, query, state['volume']) if not item: - await ctx.reply("❌ Couldn't get audio from that query.") + # Give a hint if cookies likely required + hint = "If this is YouTube, the server IP may be challenged. Provide YTDL_COOKIES_FILE or YTDL_COOKIES_B64." + await ctx.reply(f"❌ Couldn't get audio from that query.\n{hint}") return state['queue'].append(item) if not vc.is_playing() and not vc.is_paused() and not state['now']: