Initial commit: add core functionality for Patreon media downloading with UI and batch processing support
This commit is contained in:
154
with_ui.py
Normal file
154
with_ui.py
Normal file
@@ -0,0 +1,154 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user