import subprocess import threading import uuid import json import os from flask import Flask, request, jsonify, Response, stream_with_context from flask_cors import CORS import time app = Flask(__name__, static_folder='.', static_url_path='') CORS(app) # Store active jobs: {job_id: {"lines": [], "done": bool, "error": bool}} jobs = {} jobs_lock = threading.Lock() YTDLP_PATH = os.environ.get("YTDLP_PATH", "yt-dlp") DOWNLOAD_DIR = os.environ.get("DOWNLOAD_DIR", os.getcwd()) def run_ytdlp(job_id, cmd): with jobs_lock: jobs[job_id] = {"lines": [], "done": False, "error": False} try: process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=DOWNLOAD_DIR ) for line in process.stdout: line = line.rstrip() with jobs_lock: jobs[job_id]["lines"].append(line) process.wait() with jobs_lock: jobs[job_id]["done"] = True if process.returncode != 0: jobs[job_id]["error"] = True except Exception as e: with jobs_lock: jobs[job_id]["lines"].append(f"[ERROR] {str(e)}") jobs[job_id]["done"] = True jobs[job_id]["error"] = True @app.route('/') def index(): return app.send_static_file('frontend.html') @app.route('/api/info', methods=['POST']) def get_info(): data = request.json url = data.get('url', '').strip() cookies = data.get('cookies', '').strip() if not url: return jsonify({"error": "No URL provided"}), 400 cmd = [YTDLP_PATH, '--dump-json', '--no-playlist'] if cookies: cmd += ['--cookies', cookies] cmd += ['--extractor-args', 'generic:impersonate', url] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, cwd=DOWNLOAD_DIR) if result.returncode != 0: return jsonify({"error": result.stderr or "Failed to fetch info"}), 400 info = json.loads(result.stdout) formats = [] for f in info.get('formats', []): formats.append({ "id": f.get('format_id'), "ext": f.get('ext'), "resolution": f.get('resolution') or f.get('format_note') or '', "vcodec": f.get('vcodec', 'none'), "acodec": f.get('acodec', 'none'), "filesize": f.get('filesize') or f.get('filesize_approx'), "tbr": f.get('tbr'), "label": f.get('format'), }) return jsonify({ "title": info.get('title', 'Unknown'), "thumbnail": info.get('thumbnail'), "duration": info.get('duration'), "uploader": info.get('uploader'), "formats": formats }) except subprocess.TimeoutExpired: return jsonify({"error": "Timed out fetching video info"}), 408 except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/api/download', methods=['POST']) def start_download(): data = request.json url = data.get('url', '').strip() format_id = data.get('format_id', '').strip() cookies = data.get('cookies', '').strip() extra_args = data.get('extra_args', '').strip() if not url: return jsonify({"error": "No URL provided"}), 400 cmd = [YTDLP_PATH] if format_id: cmd += ['-f', format_id] cmd += ['--prefer-free-formats'] if cookies: cmd += ['--cookies', cookies] cmd += ['--extractor-args', 'generic:impersonate'] if extra_args: cmd += extra_args.split() cmd.append(url) job_id = str(uuid.uuid4()) thread = threading.Thread(target=run_ytdlp, args=(job_id, cmd), daemon=True) thread.start() return jsonify({"job_id": job_id}) @app.route('/api/status/') def job_status(job_id): def generate(): sent = 0 while True: with jobs_lock: job = jobs.get(job_id) if not job: yield f"data: {json.dumps({'error': 'Job not found'})}\n\n" break lines = job['lines'] while sent < len(lines): yield f"data: {json.dumps({'line': lines[sent]})}\n\n" sent += 1 if job['done']: yield f"data: {json.dumps({'done': True, 'error': job['error']})}\n\n" break time.sleep(0.2) return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) if __name__ == '__main__': print(f"[yt-dlp UI] Serving on http://localhost:5000") print(f"[yt-dlp UI] Download directory: {DOWNLOAD_DIR}") app.run(debug=False, host='0.0.0.0', port=5000, threaded=True)