commit b0d54efbeac56acf7ca1ac56a4b3e9f46e5ff4a3 Author: AcruxDevelopment Date: Fri Mar 20 19:37:04 2026 -0300 Add project files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23f9b91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pid +nohup.out +venv +downloads diff --git a/README.md b/README.md new file mode 100644 index 0000000..89c13da --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# VDROP — yt-dlp + Flask Downloader + +A clean web app to download videos and audio from YouTube and 1000+ other sites. + +## Requirements + +- Python 3.8+ +- `ffmpeg` installed on your system ([ffmpeg.org](https://ffmpeg.org/download.html)) + +## Setup + +```bash +# 1. Install Python dependencies +pip install -r requirements.txt + +# 2. Run the app +python app.py +``` + +Then open **http://localhost:5000** in your browser. + +## Features + +- **Video download**: MP4 output, choose resolution (best / 1080p / 720p / 480p / 360p) +- **Audio download**: Extracts audio as MP3, AAC, M4A, Opus, or FLAC +- **Video info preview**: Click "Inspect" to see thumbnail, title, uploader and real available resolutions before downloading +- **Live progress bar**: Shows download %, speed, and ETA in real time +- **One-click save**: Download button appears when the file is ready + +## Notes + +- Downloaded files are saved in the `downloads/` folder +- Works with YouTube, Vimeo, Twitter/X, TikTok, and 1000+ sites supported by yt-dlp +- FFmpeg is required for audio extraction and video merging diff --git a/app.py b/app.py new file mode 100644 index 0000000..8d7b1ee --- /dev/null +++ b/app.py @@ -0,0 +1,171 @@ +import os +import re +import json +import threading +import uuid +from flask import Flask, request, jsonify, send_file, render_template +import yt_dlp + +app = Flask(__name__) + +DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), "downloads") +os.makedirs(DOWNLOAD_DIR, exist_ok=True) + +# Track job progress +jobs = {} + +def sanitize_filename(name): + return re.sub(r'[\\/*?:"<>|]', "_", name) + +def run_download(job_id, url, fmt, quality): + jobs[job_id]["status"] = "downloading" + jobs[job_id]["progress"] = 0 + + def progress_hook(d): + if d["status"] == "downloading": + total = d.get("total_bytes") or d.get("total_bytes_estimate", 0) + downloaded = d.get("downloaded_bytes", 0) + if total: + pct = int(downloaded / total * 100) + jobs[job_id]["progress"] = pct + jobs[job_id]["speed"] = d.get("_speed_str", "") + jobs[job_id]["eta"] = d.get("_eta_str", "") + elif d["status"] == "finished": + jobs[job_id]["progress"] = 99 + + try: + import shutil + + common = { + "progress_hooks": [progress_hook], + "quiet": True, + "extractor_args": {"youtube": { + "js_runtimes": [f"node:/usr/bin/node"] + }}, + } + + if fmt == "audio": + ydl_opts = { + **common, + "format": "bestaudio/best", + "outtmpl": os.path.join(DOWNLOAD_DIR, f"{job_id}_%(title)s.%(ext)s"), + "postprocessors": [{ + "key": "FFmpegExtractAudio", + "preferredcodec": quality or "mp3", + "preferredquality": "192", + }], + } + else: + res = quality or "best" + if res == "best": + fmt_str = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" + else: + h = res.replace("p", "") + fmt_str = f"bestvideo[height<={h}][ext=mp4]+bestaudio[ext=m4a]/best[height<={h}][ext=mp4]/best" + + ydl_opts = { + **common, + "format": fmt_str, + "outtmpl": os.path.join(DOWNLOAD_DIR, f"{job_id}_%(title)s.%(ext)s"), + "merge_output_format": "mp4", + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + title = info.get("title", "video") + + # Find the downloaded file + files = [f for f in os.listdir(DOWNLOAD_DIR) if f.startswith(job_id)] + if files: + jobs[job_id]["status"] = "done" + jobs[job_id]["progress"] = 100 + jobs[job_id]["filename"] = files[0] + jobs[job_id]["title"] = title + else: + jobs[job_id]["status"] = "error" + jobs[job_id]["error"] = "File not found after download." + + except Exception as e: + jobs[job_id]["status"] = "error" + jobs[job_id]["error"] = str(e) + + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/info", methods=["POST"]) +def get_info(): + data = request.get_json() + url = data.get("url", "").strip() + if not url: + return jsonify({"error": "No URL provided"}), 400 + try: + import shutil + node_bin = shutil.which("node") or shutil.which("nodejs") or "/usr/bin/node" + ydl_opts = { + "quiet": True, + "skip_download": True, + "extractor_args": {"youtube": { + "js_runtimes": [f"node:{node_bin}"] + }}, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + formats = info.get("formats", []) + heights = sorted(set( + f.get("height") for f in formats + if f.get("height") and f.get("vcodec") != "none" + ), reverse=True) + return jsonify({ + "title": info.get("title"), + "thumbnail": info.get("thumbnail"), + "duration": info.get("duration"), + "uploader": info.get("uploader"), + "resolutions": [f"{h}p" for h in heights[:6]], + }) + except Exception as e: + return jsonify({"error": str(e)}), 400 + + +@app.route("/download", methods=["POST"]) +def start_download(): + data = request.get_json() + url = data.get("url", "").strip() + fmt = data.get("format", "video") + quality = data.get("quality", "best") + + if not url: + return jsonify({"error": "No URL provided"}), 400 + + job_id = str(uuid.uuid4())[:8] + jobs[job_id] = {"status": "queued", "progress": 0} + + t = threading.Thread(target=run_download, args=(job_id, url, fmt, quality), daemon=True) + t.start() + + return jsonify({"job_id": job_id}) + + +@app.route("/status/") +def job_status(job_id): + job = jobs.get(job_id) + if not job: + return jsonify({"error": "Job not found"}), 404 + return jsonify(job) + + +@app.route("/file/") +def download_file(job_id): + job = jobs.get(job_id) + if not job or job.get("status") != "done": + return jsonify({"error": "File not ready"}), 404 + filepath = os.path.join(DOWNLOAD_DIR, job["filename"]) + if not os.path.exists(filepath): + return jsonify({"error": "File missing"}), 404 + return send_file(filepath, as_attachment=True, download_name=job["filename"].split("_", 1)[-1]) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..049df4f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0.0 +yt-dlp>=2024.1.1 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..bad2121 --- /dev/null +++ b/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash +FILE=".pid" + +if [ -f $FILE ]; then + ./stop.sh +fi +fuser -k 5000/tcp +source venv/bin/activate +nohup python app.py & +echo $! > $FILE diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..e9c70c9 --- /dev/null +++ b/stop.sh @@ -0,0 +1,10 @@ +FILE=".pid" + +stop() { + kill $(cat $FILE) + rm $FILE +} + +if [ -f $FILE ]; then + stop +fi diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..8057b9e --- /dev/null +++ b/templates/index.html @@ -0,0 +1,588 @@ + + + + + +VDROP — Video Downloader + + + + + + +
+ +
yt-dlp powered · audio & video
+
+ +
+ + + +
+ + +
+ + +
+
thumb
+
+
—
+
—
+
+
+ + +
+ + + +
+ + +
+ + +
+ +
+ + + + + +
+
+ + +
+ +
+
+
+
+ + 0% +
+
+ + +
+ + +
+ +
+ +
yt-dlp + flask · for personal use only · respect copyright
+ + + +