# Monkey patch MUST be first - before any other imports! import eventlet eventlet.monkey_patch() import os from flask import Flask, send_from_directory, jsonify, request, session from flask_socketio import SocketIO, emit from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() import downloader # Relay State broadcast_state = { 'active': False } listener_count = 0 MUSIC_FOLDER = "music" # Ensure music folder exists if not os.path.exists(MUSIC_FOLDER): os.makedirs(MUSIC_FOLDER) # Helper for shared routes def setup_shared_routes(app): @app.route('/library.json') def get_library(): library = [] if os.path.exists(MUSIC_FOLDER): for filename in sorted(os.listdir(MUSIC_FOLDER)): if filename.lower().endswith(('.mp3', '.m4a', '.wav', '.flac', '.ogg')): library.append({ "title": os.path.splitext(filename)[0], "file": f"music/{filename}" }) return jsonify(library) @app.route('/download', methods=['POST']) def download(): data = request.json url = data.get('url') quality = data.get('quality', '320') if not url: return jsonify({"success": False, "error": "No URL provided"}), 400 result = downloader.download_mp3(url, quality) return jsonify(result) @app.route('/search_youtube', methods=['GET']) def search_youtube(): query = request.args.get('q', '') if not query: return jsonify({"success": False, "error": "No query provided"}), 400 # Get API key from environment variable api_key = os.environ.get('YOUTUBE_API_KEY', '') if not api_key: return jsonify({ "success": False, "error": "YouTube API key not configured. Set YOUTUBE_API_KEY environment variable." }), 500 try: import requests # Search YouTube using Data API v3 url = 'https://www.googleapis.com/youtube/v3/search' params = { 'part': 'snippet', 'q': query, 'type': 'video', 'videoCategoryId': '10', # Music category 'maxResults': 20, 'key': api_key } response = requests.get(url, params=params) data = response.json() if 'error' in data: return jsonify({ "success": False, "error": data['error'].get('message', 'YouTube API error') }), 400 # Format results results = [] for item in data.get('items', []): results.append({ 'videoId': item['id']['videoId'], 'title': item['snippet']['title'], 'channel': item['snippet']['channelTitle'], 'thumbnail': item['snippet']['thumbnails']['medium']['url'], 'url': f"https://www.youtube.com/watch?v={item['id']['videoId']}" }) return jsonify({"success": True, "results": results}) except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 @app.route('/') def serve_static(filename): response = send_from_directory('.', filename) if filename.endswith(('.css', '.js', '.html')): response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' return response @app.route('/') def index(): return send_from_directory('.', 'index.html') @app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return jsonify({"success": False, "error": "No file provided"}), 400 file = request.files['file'] if file.filename == '': return jsonify({"success": False, "error": "No file selected"}), 400 if not file.filename.endswith('.mp3'): return jsonify({"success": False, "error": "Only MP3 files are allowed"}), 400 # Sanitize filename (keep extension) import re name_without_ext = os.path.splitext(file.filename)[0] name_without_ext = re.sub(r'[^\w\s-]', '', name_without_ext) name_without_ext = re.sub(r'[-\s]+', '-', name_without_ext) filename = f"{name_without_ext}.mp3" filepath = os.path.join(MUSIC_FOLDER, filename) try: file.save(filepath) print(f"✅ Uploaded: {filename}") return jsonify({"success": True, "filename": filename}) except Exception as e: print(f"❌ Upload error: {e}") return jsonify({"success": False, "error": str(e)}), 500 # === DJ SERVER (Port 5000) === dj_app = Flask(__name__, static_folder='.', static_url_path='') dj_app.config['SECRET_KEY'] = 'dj_panel_secret' setup_shared_routes(dj_app) dj_socketio = SocketIO( dj_app, cors_allowed_origins="*", async_mode='eventlet', max_http_buffer_size=1e8, # 100MB buffer ping_timeout=10, ping_interval=5, logger=False, engineio_logger=False ) @dj_socketio.on('connect') def dj_connect(): print(f"🎧 DJ connected: {request.sid}") session['is_dj'] = True @dj_socketio.on('disconnect') def dj_disconnect(): if session.get('is_dj'): print("⚠️ DJ disconnected - broadcast will continue until manually stopped") session['is_dj'] = False # Don't stop streaming_active - let it continue # Broadcast will resume when DJ reconnects def stop_broadcast_after_timeout(): """No longer used - broadcasts don't auto-stop""" pass @dj_socketio.on('start_broadcast') def dj_start(): broadcast_state['active'] = True session['is_dj'] = True print("🎙️ Broadcast -> ACTIVE") listener_socketio.emit('broadcast_started', namespace='/') listener_socketio.emit('stream_status', {'active': True}, namespace='/') @dj_socketio.on('stop_broadcast') def dj_stop(): broadcast_state['active'] = False session['is_dj'] = False print("🛑 DJ stopped broadcasting") listener_socketio.emit('broadcast_stopped', namespace='/') listener_socketio.emit('stream_status', {'active': False}, namespace='/') @dj_socketio.on('audio_chunk') def dj_audio(data): # Relay audio chunk to all listeners immediately if broadcast_state['active']: listener_socketio.emit('audio_data', data, namespace='/') # === LISTENER SERVER (Port 6000) === listener_app = Flask(__name__, static_folder='.', static_url_path='') listener_app.config['SECRET_KEY'] = 'listener_secret' setup_shared_routes(listener_app) listener_socketio = SocketIO( listener_app, cors_allowed_origins="*", async_mode='eventlet', max_http_buffer_size=1e8, # 100MB buffer ping_timeout=10, ping_interval=5, logger=False, engineio_logger=False ) @listener_socketio.on('connect') def listener_connect(): print(f"👂 Listener Socket Connected: {request.sid}") @listener_socketio.on('disconnect') def listener_disconnect(): global listener_count if session.get('is_listener'): # Clear session flag FIRST to prevent re-entry issues session['is_listener'] = False listener_count = max(0, listener_count - 1) print(f"❌ Listener left. Total: {listener_count}") # Broadcast to all listeners listener_socketio.emit('listener_count', {'count': listener_count}, namespace='/') # Broadcast to all DJs dj_socketio.emit('listener_count', {'count': listener_count}, namespace='/') @listener_socketio.on('join_listener') def listener_join(): global listener_count if not session.get('is_listener'): session['is_listener'] = True listener_count += 1 print(f"👂 New listener joined. Total: {listener_count}") # Broadcast to all listeners listener_socketio.emit('listener_count', {'count': listener_count}, namespace='/') # Broadcast to all DJs dj_socketio.emit('listener_count', {'count': listener_count}, namespace='/') emit('stream_status', {'active': broadcast_state['active']}) @listener_socketio.on('request_header') def handle_request_header(): # Header logic removed for local relay mode pass @listener_socketio.on('get_listener_count') def listener_get_count(): emit('listener_count', {'count': listener_count}) # DJ Panel Routes (No engine commands needed in local mode) @dj_socketio.on('get_mixer_status') def get_mixer_status(): pass @dj_socketio.on('audio_sync_queue') def audio_sync_queue(data): pass if __name__ == '__main__': print("=" * 50) print("🎧 TECHDJ PRO - DUAL PORT ARCHITECTURE") print("=" * 50) print("👉 DJ PANEL: http://localhost:5000") print("👉 LISTEN PAGE: http://localhost:5001") print("=" * 50) # Audio engine DISABLED print("✅ Local Radio server ready") # Run both servers using eventlet's spawn eventlet.spawn(dj_socketio.run, dj_app, host='0.0.0.0', port=5000, debug=False) listener_socketio.run(listener_app, host='0.0.0.0', port=5001, debug=False)