Guest User

LetItEnd.py

a guest
Apr 19th, 2026
213
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.51 KB | Help | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. AudioChan Stream Capture Tool (Manual Mode Only)
  4. Uses Playwright to capture stream URLs and full headers (including HttpOnly cookies).
  5. Generates the robust FFmpeg command for manual copy-pasting.
  6. """
  7. import argparse
  8. import asyncio
  9. from playwright.async_api import async_playwright
  10. import os
  11. import re
  12. import sys
  13. import base64
  14. from urllib.parse import urlparse, parse_qs
  15.  
  16. # Force UTF-8 output so emojis aren't mangled on Windows
  17. sys.stdout.reconfigure(encoding='utf-8')
  18. if sys.platform == 'win32':
  19.     os.system('chcp 65001 > nul')
  20.  
  21. # Resolve directories relative to this script's location
  22. SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
  23.  
  24. # Path to store browser session (cookies, etc.) so you don't have to login every time
  25. USER_DATA_DIR = os.path.join(SCRIPT_DIR, "playwright_data")
  26. os.makedirs(USER_DATA_DIR, exist_ok=True)
  27.  
  28. # Output directory for captured audio files
  29. OUTPUT_DIR = os.path.join(SCRIPT_DIR, "audiochan output")
  30. os.makedirs(OUTPUT_DIR, exist_ok=True)
  31.  
  32. async def capture_stream_manual(url):
  33.     print(f"🚀 Launching browser for: {url}")
  34.     print(f"📂 User Data Dir: {USER_DATA_DIR}")
  35.  
  36.     async with async_playwright() as p:
  37.         # Launch persistent context to save login state
  38.         context = await p.chromium.launch_persistent_context(
  39.             user_data_dir=USER_DATA_DIR,
  40.             headless=False,  # Change if cloudflare is an issue
  41.             channel="chrome", # Try to use installed Chrome if available for better realism
  42.             args=[
  43.                 '--disable-blink-features=AutomationControlled', # Hide automation flag
  44.                 '--disable-infobars',
  45.                 '--no-sandbox',
  46.                 '--window-size=1920,1080',
  47.                 '--start-maximized',
  48.             ]
  49.         )
  50.        
  51.         page = await context.new_page()
  52.  
  53.         # storage for captured info
  54.         stream_info = {
  55.             'url': None,
  56.             'headers': None
  57.         }
  58.  
  59.         # Event listener for requests
  60.         async def handle_request(request):
  61.             try:
  62.                 # Print ALL requests so we can see what's happening
  63.                 print(f"  [REQUEST] {request.url}")
  64.                
  65.                 # Filter strictly for the stream URL
  66.                 if 'stream.audiochan.com' in request.url and '&st=' in request.url:
  67.                     print(f"\n✅ CAPTURED STREAM REQUEST! (Updating...)")
  68.                     stream_info['url'] = request.url
  69.                     try:
  70.                         # This can sometimes fail if the request is closed too quickly
  71.                         stream_info['headers'] = await request.all_headers()
  72.                     except Exception as e:
  73.                         print(f"⚠️ Warning: Could not fetch full headers: {e}")
  74.                         # Fallback to basic headers if available, or just use what we have
  75.                         if not stream_info['headers']: # Only overwrite if we don't have good headers yet
  76.                             stream_info['headers'] = request.headers
  77.             except Exception as e:
  78.                 # Catch-all for any other issues in the handler to prevent loop crash
  79.                 pass
  80.  
  81.         page.on("request", handle_request)
  82.  
  83.         try:
  84.             print("⏳ Navigating to page...")
  85.             await page.goto(url)
  86.            
  87.             print("👀 Waiting for stream to start... (Press Play on the page if needed)")
  88.            
  89.             # Wait until we capture the stream AND it stabilizes
  90.             stability_count = 0
  91.             required_stability = 3
  92.            
  93.             for _ in range(60):
  94.                 if stream_info['url'] and stream_info['headers']:
  95.                     stability_count += 1
  96.                     print(f"   Stream detected... stabilizing ({stability_count}/{required_stability})")
  97.                     if stability_count >= required_stability:
  98.                         break
  99.                 else:
  100.                     stability_count = 0 # Reset if lost (unlikely but good safety)
  101.                
  102.                 await asyncio.sleep(1)
  103.            
  104.             if stream_info['url'] and stream_info['headers']:
  105.                
  106.                 # Sanitize title: Replace invalid Windows filename characters with underscore
  107.                 # Invalid characters for Windows filenames: < > : " / \ | ? *
  108.                 invalid_chars = r'[<>:"/\\|?*]'
  109.                
  110.                 # Wait for the h1 to have content
  111.                 await page.wait_for_selector('h1.text-foreground:not(:empty)')
  112.  
  113.                 h1_title = await page.evaluate('''() => {
  114.                    const el = document.querySelector('h1.text-foreground');
  115.                    return el ? el.innerText.trim() : null;
  116.                }''')
  117.                
  118.                 title = h1_title or await page.title()
  119.                 safe_title = re.sub(invalid_chars, '_', title).strip()
  120.                 safe_title = re.sub(r'[ .]+$', '', safe_title)
  121.                 output_filename = f"{safe_title}.mp4"
  122.                
  123.                 # Grab artist from span, strip the "u/" prefix
  124.                 artist = await page.evaluate('''() => {
  125.                    const el = document.querySelector('span.truncate.text-muted-foreground.text-sm.no-underline');
  126.                    return el ? el.innerText.trim().replace(/^u\//, '') : null;
  127.                }''')
  128.                 artist = artist or ""
  129.                
  130.                 # Grab description from <em> tag
  131.                 description = await page.evaluate('''() => {
  132.                    const el = document.querySelector('p');
  133.                    return el ? el.innerText.trim() : null;
  134.                }''')
  135.                
  136.                 description = description.replace('"', '\\"') if description else ""
  137.                
  138.                 safe_title = re.sub(invalid_chars, '_', title).strip()
  139.                 # Also remove any trailing periods or spaces that Windows dislikes
  140.                 safe_title = re.sub(r'[ .]+$', '', safe_title)
  141.                
  142.                 print("\n" + "="*80)
  143.                 print("🎉 SUCCESS! Stream info captured.")
  144.                 print("="*80)
  145.  
  146.                 # Construct FFmpeg Command
  147.                 headers_dict = stream_info['headers']
  148.                
  149.                 # Format headers for FFmpeg (-headers "Key: Value\r\nKey: Value\r\n")
  150.                 ffmpeg_headers = ""
  151.                
  152.                 # Critical headers
  153.                 keys_to_keep = ['user-agent', 'referer', 'cookie', 'accept', 'accept-language', 'origin']
  154.                
  155.                 for key, value in headers_dict.items():
  156.                     if key.lower() in keys_to_keep:
  157.                         # Escape double quotes inside header values with a backslash
  158.                         safe_val = value.replace('"', '\\"')
  159.                         # Capitalize keys nicely
  160.                         nice_key = key.title()
  161.                         ffmpeg_headers += f"{nice_key}: {safe_val}\r\n"
  162.                
  163.                
  164.                 # Construct the full command
  165.                 # Note: We use .mp4 container for better compatibility with streams
  166.                 # Decode t= to get the real format
  167.                 parsed = urlparse(stream_info['url'])
  168.                 t_param = parse_qs(parsed.query).get('t', [''])[0]
  169.                 try:
  170.                     decoded = base64.b64decode(t_param + '==').decode('utf-8', errors='ignore')
  171.                     ext = decoded.split('.')[-1]
  172.                     print(f"🔍 Detected format: {ext}")
  173.                 except:
  174.                     ext = 'mp4'
  175.                     print(f"⚠️ Could not detect format, defaulting to mp4")
  176.  
  177.                 if ext == 'wav':
  178.                     # Convert WAV to M4A (AAC, high quality)
  179.                     output_filename = os.path.join(OUTPUT_DIR, f"{safe_title}.m4a")
  180.                     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}"'
  181.                     print(f"🎵 WAV detected — converting to M4A (256k AAC)")
  182.                 else:
  183.                     output_filename = os.path.join(OUTPUT_DIR, f"{safe_title}.{ext}")
  184.                     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}"'
  185.                 print(f"🎵 Output: {output_filename}")              
  186.                
  187.                 print(f"📁 Output path: {output_filename}")
  188.                
  189.                 print("\n📋 FFmpeg Command (Auto-Generated):")
  190.                 print("-" * 20)
  191.                 print(cmd_str.encode('utf-8').decode('utf-8'))
  192.                 print("-" * 20)
  193.                 print("\n⚠️  Copy the command above and run it in your terminal.")
  194.             else:
  195.                 print("\n❌ Timeout: Stream request not found.")
  196.                 print("Did you play the audio? Is the site asking for login?")
  197.  
  198.         except Exception as e:
  199.             print(f"\n❌ Error: {e}")
  200.        
  201.         finally:
  202.             # Safely close the browser context
  203.             try:
  204.                 if context.pages: # Simple check if it might still be open
  205.                     print("\nClosing browser...")
  206.                     await context.close()
  207.             except Exception as e:
  208.                 # Ignore errors during shutdown (e.g., Target closed, Loop closed)
  209.                 pass
  210.  
  211. def main():
  212.     if sys.platform == 'win32':
  213.         asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
  214.  
  215.     parser = argparse.ArgumentParser(description="AudioChan Stream Capture Tool (Manual Mode Only)")
  216.     parser.add_argument('url', nargs='?', help='The URL of the AudioChan page (optional)')
  217.     args = parser.parse_args()
  218.  
  219.     target_url = args.url
  220.  
  221.     # Interactive URL Input if not provided
  222.     if not target_url:
  223.         print("Enter the AudioChan URL:")
  224.         target_url = input("> ").strip()
  225.         if not target_url:
  226.             print("Error: No URL provided. Exiting.")
  227.             return
  228.  
  229.     asyncio.run(capture_stream_manual(target_url))
  230.  
  231. if __name__ == "__main__":
  232.     main()
Add Comment
Please, Sign In to add comment