Some checks failed
Code Check - Quality and Syntax / syntax-lint (3.14) (push) Has been cancelled
Code Check - Quality and Syntax / syntax-lint (3.12) (push) Has been cancelled
Code Check - Quality and Syntax / syntax-lint (3.11) (push) Has been cancelled
Code Check - Quality and Syntax / syntax-lint (3.13) (push) Has been cancelled
173 lines
5.1 KiB
Python
173 lines
5.1 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:
|
|
msg = result.stderr or "Failed to fetch info"
|
|
return jsonify({"error": msg}), 400
|
|
info = json.loads(result.stdout)
|
|
formats = []
|
|
for f in info.get("formats", []):
|
|
res = f.get("resolution") or f.get("format_note") or ""
|
|
formats.append(
|
|
{
|
|
"id": f.get("format_id"),
|
|
"ext": f.get("ext"),
|
|
"resolution": res,
|
|
"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:
|
|
payload = json.dumps({"error": "Job not found"})
|
|
yield "data: " + payload + "\n\n"
|
|
break
|
|
lines = job["lines"]
|
|
while sent < len(lines):
|
|
payload = json.dumps({"line": lines[sent]})
|
|
yield "data: " + payload + "\n\n"
|
|
sent += 1
|
|
if job["done"]:
|
|
payload = json.dumps({"done": True, "error": job["error"]})
|
|
yield "data: " + payload + "\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("[yt-dlp UI] Serving on http://localhost:5000")
|
|
print("[yt-dlp UI] Download directory: {}".format(DOWNLOAD_DIR))
|
|
app.run(debug=False, host="0.0.0.0", port=5000, threaded=True)
|