154 lines
4.8 KiB
Python
154 lines
4.8 KiB
Python
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/<job_id>')
|
|
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) |