Files
patreon-downloader/with_ui.py
Space-Banane f34c90b999
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
refactor: improve error handling and response formatting in API endpoints
2026-04-06 23:09:06 +02:00

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)