Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- AudioChan Stream Capture Tool (Manual Mode Only)
- Uses Playwright to capture stream URLs and full headers (including HttpOnly cookies).
- Generates the robust FFmpeg command for manual copy-pasting.
- """
- import argparse
- import asyncio
- from playwright.async_api import async_playwright
- import os
- import re
- import sys
- import base64
- from urllib.parse import urlparse, parse_qs
- # Force UTF-8 output so emojis aren't mangled on Windows
- sys.stdout.reconfigure(encoding='utf-8')
- if sys.platform == 'win32':
- os.system('chcp 65001 > nul')
- # Resolve directories relative to this script's location
- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
- # Path to store browser session (cookies, etc.) so you don't have to login every time
- USER_DATA_DIR = os.path.join(SCRIPT_DIR, "playwright_data")
- os.makedirs(USER_DATA_DIR, exist_ok=True)
- # Output directory for captured audio files
- OUTPUT_DIR = os.path.join(SCRIPT_DIR, "audiochan output")
- os.makedirs(OUTPUT_DIR, exist_ok=True)
- async def capture_stream_manual(url):
- print(f"🚀 Launching browser for: {url}")
- print(f"📂 User Data Dir: {USER_DATA_DIR}")
- async with async_playwright() as p:
- # Launch persistent context to save login state
- context = await p.chromium.launch_persistent_context(
- user_data_dir=USER_DATA_DIR,
- headless=False, # Change if cloudflare is an issue
- channel="chrome", # Try to use installed Chrome if available for better realism
- args=[
- '--disable-blink-features=AutomationControlled', # Hide automation flag
- '--disable-infobars',
- '--no-sandbox',
- '--window-size=1920,1080',
- '--start-maximized',
- ]
- )
- page = await context.new_page()
- # storage for captured info
- stream_info = {
- 'url': None,
- 'headers': None
- }
- # Event listener for requests
- async def handle_request(request):
- try:
- # Print ALL requests so we can see what's happening
- print(f" [REQUEST] {request.url}")
- # Filter strictly for the stream URL
- if 'stream.audiochan.com' in request.url and '&st=' in request.url:
- print(f"\n✅ CAPTURED STREAM REQUEST! (Updating...)")
- stream_info['url'] = request.url
- try:
- # This can sometimes fail if the request is closed too quickly
- stream_info['headers'] = await request.all_headers()
- except Exception as e:
- print(f"⚠️ Warning: Could not fetch full headers: {e}")
- # Fallback to basic headers if available, or just use what we have
- if not stream_info['headers']: # Only overwrite if we don't have good headers yet
- stream_info['headers'] = request.headers
- except Exception as e:
- # Catch-all for any other issues in the handler to prevent loop crash
- pass
- page.on("request", handle_request)
- try:
- print("⏳ Navigating to page...")
- await page.goto(url)
- print("👀 Waiting for stream to start... (Press Play on the page if needed)")
- # Wait until we capture the stream AND it stabilizes
- stability_count = 0
- required_stability = 3
- for _ in range(60):
- if stream_info['url'] and stream_info['headers']:
- stability_count += 1
- print(f" Stream detected... stabilizing ({stability_count}/{required_stability})")
- if stability_count >= required_stability:
- break
- else:
- stability_count = 0 # Reset if lost (unlikely but good safety)
- await asyncio.sleep(1)
- if stream_info['url'] and stream_info['headers']:
- # Sanitize title: Replace invalid Windows filename characters with underscore
- # Invalid characters for Windows filenames: < > : " / \ | ? *
- invalid_chars = r'[<>:"/\\|?*]'
- # Wait for the h1 to have content
- await page.wait_for_selector('h1.text-foreground:not(:empty)')
- h1_title = await page.evaluate('''() => {
- const el = document.querySelector('h1.text-foreground');
- return el ? el.innerText.trim() : null;
- }''')
- title = h1_title or await page.title()
- safe_title = re.sub(invalid_chars, '_', title).strip()
- safe_title = re.sub(r'[ .]+$', '', safe_title)
- output_filename = f"{safe_title}.mp4"
- # Grab artist from span, strip the "u/" prefix
- artist = await page.evaluate('''() => {
- const el = document.querySelector('span.truncate.text-muted-foreground.text-sm.no-underline');
- return el ? el.innerText.trim().replace(/^u\//, '') : null;
- }''')
- artist = artist or ""
- # Grab description from <em> tag
- description = await page.evaluate('''() => {
- const el = document.querySelector('p');
- return el ? el.innerText.trim() : null;
- }''')
- description = description.replace('"', '\\"') if description else ""
- safe_title = re.sub(invalid_chars, '_', title).strip()
- # Also remove any trailing periods or spaces that Windows dislikes
- safe_title = re.sub(r'[ .]+$', '', safe_title)
- print("\n" + "="*80)
- print("🎉 SUCCESS! Stream info captured.")
- print("="*80)
- # Construct FFmpeg Command
- headers_dict = stream_info['headers']
- # Format headers for FFmpeg (-headers "Key: Value\r\nKey: Value\r\n")
- ffmpeg_headers = ""
- # Critical headers
- keys_to_keep = ['user-agent', 'referer', 'cookie', 'accept', 'accept-language', 'origin']
- for key, value in headers_dict.items():
- if key.lower() in keys_to_keep:
- # Escape double quotes inside header values with a backslash
- safe_val = value.replace('"', '\\"')
- # Capitalize keys nicely
- nice_key = key.title()
- ffmpeg_headers += f"{nice_key}: {safe_val}\r\n"
- # Construct the full command
- # Note: We use .mp4 container for better compatibility with streams
- # Decode t= to get the real format
- parsed = urlparse(stream_info['url'])
- t_param = parse_qs(parsed.query).get('t', [''])[0]
- try:
- decoded = base64.b64decode(t_param + '==').decode('utf-8', errors='ignore')
- ext = decoded.split('.')[-1]
- print(f"🔍 Detected format: {ext}")
- except:
- ext = 'mp4'
- print(f"⚠️ Could not detect format, defaulting to mp4")
- if ext == 'wav':
- # Convert WAV to M4A (AAC, high quality)
- output_filename = os.path.join(OUTPUT_DIR, f"{safe_title}.m4a")
- cmd_str = f'ffmpeg -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -headers "{ffmpeg_headers}" -i "{stream_info["url"]}" -vn -c:a aac -b:a 256k -movflags +faststart -metadata title="{safe_title}" -metadata artist="{artist}" -metadata comment="{description}" -metadata genre="" -metadata composer="" "{output_filename}"'
- print(f"🎵 WAV detected — converting to M4A (256k AAC)")
- else:
- output_filename = os.path.join(OUTPUT_DIR, f"{safe_title}.{ext}")
- cmd_str = f'ffmpeg -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -headers "{ffmpeg_headers}" -i "{stream_info["url"]}" -vn -c:a copy -movflags +faststart -metadata title="{safe_title}" -metadata artist="{artist}" -metadata comment="{description}" -metadata genre="" -metadata composer="" "{output_filename}"'
- print(f"🎵 Output: {output_filename}")
- print(f"📁 Output path: {output_filename}")
- print("\n📋 FFmpeg Command (Auto-Generated):")
- print("-" * 20)
- print(cmd_str.encode('utf-8').decode('utf-8'))
- print("-" * 20)
- print("\n⚠️ Copy the command above and run it in your terminal.")
- else:
- print("\n❌ Timeout: Stream request not found.")
- print("Did you play the audio? Is the site asking for login?")
- except Exception as e:
- print(f"\n❌ Error: {e}")
- finally:
- # Safely close the browser context
- try:
- if context.pages: # Simple check if it might still be open
- print("\nClosing browser...")
- await context.close()
- except Exception as e:
- # Ignore errors during shutdown (e.g., Target closed, Loop closed)
- pass
- def main():
- if sys.platform == 'win32':
- asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
- parser = argparse.ArgumentParser(description="AudioChan Stream Capture Tool (Manual Mode Only)")
- parser.add_argument('url', nargs='?', help='The URL of the AudioChan page (optional)')
- args = parser.parse_args()
- target_url = args.url
- # Interactive URL Input if not provided
- if not target_url:
- print("Enter the AudioChan URL:")
- target_url = input("> ").strip()
- if not target_url:
- print("Error: No URL provided. Exiting.")
- return
- asyncio.run(capture_stream_manual(target_url))
- if __name__ == "__main__":
- main()
Add Comment
Please, Sign In to add comment