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)