🎬 Project: Building a Universal Video Downloader (TikTok/YouTube)
Goal: Create a high-speed, watermark-free video downloader using a Python (Flask) backend for the heavy lifting and a PHP/HTML frontend for the user interface.
The Architecture:
- The Brain (Backend): A Python Flask API hosted on a subdomain (e.g., api.yourdomain.com). This handles the yt-dlp processing and FFmpeg conversions.
- The Face (Frontend): A clean HTML/Blade page or WordPress shortcode on your main domain (e.g., yourdomain.com/tiktok) that talks to the API via AJAX.
🏗️ PHASE 1: The Backend (The Engine)
We will first build the API that performs the actual downloading and processing. We are using cPanel for this setup, which requires a few specific tricks to get FFmpeg and Python working together without root access.
Step 1: Create the Subdomain
- Log in to cPanel.
- Navigate to Domains.
- Create a new domain: api.yourdomain.com.
- Important: Ensure the "Document Root" is separate from your main site (e.g., /home/username/api.yourdomain.com).
Step 2: Create the Python Application
- Go to cPanel > Setup Python App.
- Click Create Application.
- Python Version: Select 3.11 (recommended).
- Application Root: my_video_backend.
- Application URL: Select your new subdomain (api.yourdomain.com).
- Click Create.
Step 3: Install FFmpeg (No Root Required)
Most shared hosts don't have FFmpeg installed, or they have an old version. We will install a static binary manually.
- Open Terminal in cPanel (or use SSH).
- Run the following commands one by one to download and install FFmpeg to your user directory:
Bash
# Move to your home directory
cd ~
# Create a folder for the binary
mkdir ~/ffmpeg_bin
cd ~/ffmpeg_bin
# Download the latest static Linux build
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
# Extract the files
tar xvf ffmpeg-release-amd64-static.tar.xz
# Move the binary to the main folder and clean up
cd ffmpeg-*-static/
mv ffmpeg ..
cd ..
chmod +x ffmpeg
rm -rf ffmpeg-*-static/
rm -f ffmpeg-release-amd64-static.tar.xz
Verify the install:
Bash
~/ffmpeg_bin/ffmpeg -version
You should see output starting with "ffmpeg version...".
Step 4: The Backend Code
Go to File Manager > my_video_backend and create the following three files.
File 1: requirements.txt
After creating this, go back to the "Setup Python App" page in cPanel and click Run Pip Install.
Plaintext
flask
flask-cors
yt-dlp
requests
File 2: app.py (The Master Logic)
Update the paths in the CONFIGURATION section to match your actual cPanel username.
Python
import sys, os, time, random, urllib.parse, logging, gc
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import yt_dlp
# --- CONFIGURATION ---
# UPDATE THIS PATH:
BASE_DIR = "/home/your_username/my_video_backend"
DOWNLOAD_DIR = os.path.join(BASE_DIR, 'downloads')
# UPDATE THIS PATH:
FFMPEG_EXE = "/home/your_username/ffmpeg_bin/ffmpeg"
COOKIE_PATH = os.path.join(BASE_DIR, 'cookies.txt')
LOG_FILE = os.path.join(BASE_DIR, 'debug_log.txt')
# --- LOGGING ---
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s - %(message)s')
app = Flask(__name__)
# --- SECURITY: ALLOWED DOMAINS ---
ALLOWED_DOMAINS = [
"yourdomain.com",
"www.yourdomain.com"
]
CORS(app, resources={r"/*": {"origins": "*"}})
if not os.path.exists(DOWNLOAD_DIR):
os.makedirs(DOWNLOAD_DIR, mode=0o755, exist_ok=True)
# --- TRIGGER-BASED CLEANUP (Safe for cPanel) ---
def run_cleanup_check():
try:
# Only run cleanup with 10% probability to save resources
if random.random() > 0.1: return
now = time.time()
for f in os.listdir(DOWNLOAD_DIR):
f_path = os.path.join(DOWNLOAD_DIR, f)
# Delete files older than 20 mins
if os.stat(f_path).st_mtime < now - 1200:
os.remove(f_path)
logging.info("Cleanup check completed.")
except Exception: pass
def is_request_allowed(req):
origin = req.headers.get('Origin')
referer = req.headers.get('Referer')
if not origin and not referer: return False
domain_check = origin or referer
for allowed in ALLOWED_DOMAINS:
if allowed in domain_check: return True
return False
@app.route('/', methods=['GET'])
def home():
return jsonify({"status": "Online", "engine": "V6.0 (No-Thread)"})
@app.route('/get-link/', methods=['POST'])
def get_link():
run_cleanup_check()
if not is_request_allowed(request):
return jsonify({"success": False, "error": "Unauthorized"}), 403
try:
content = request.get_json(silent=True)
if not content or 'url' not in content:
return jsonify({"success": False, "error": "No URL provided"}), 400
video_url = content['url']
format_choice = content.get('format_type', 'mp4')
file_id = f"{int(time.time())}_{random.randint(1000,9999)}"
ydl_opts = {
'outtmpl': os.path.join(DOWNLOAD_DIR, f'{file_id}_%(title)s.%(ext)s'),
'ffmpeg_location': FFMPEG_EXE,
'cookiefile': COOKIE_PATH if os.path.exists(COOKIE_PATH) else None,
'quiet': True,
'nocheckcertificate': True,
'force_ipv4': True,
'socket_timeout': 20,
'format': 'best[ext=mp4]/bestvideo+bestaudio/best',
'extractor_args': {'tiktok': {'app_version': '30.0.0', 'manifest_app_version': '30.0.0'}},
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
if format_choice == 'mp3':
ydl_opts['format'] = 'bestaudio/best'
ydl_opts['postprocessors'] = [{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3','preferredquality': '192'}]
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_url, download=True)
filename = ydl.prepare_filename(info)
if format_choice == 'mp3' and not filename.endswith('.mp3'):
filename = os.path.splitext(filename)[0] + ".mp3"
if not os.path.exists(filename) or os.path.getsize(filename) == 0:
return jsonify({"success": False, "error": "Download failed."}), 500
gc.collect()
# UPDATE THIS DOMAIN:
download_link = f"https://api.yourdomain.com/serve-video/{urllib.parse.quote(os.path.basename(filename))}"
return jsonify({
"success": True,
"title": info.get('title', 'Video Download'),
"thumbnail": info.get('thumbnail', ''),
"size_mb": round(os.path.getsize(filename) / (1024 * 1024), 2),
"download_url": download_link
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/serve-video/<path:filename>')
def serve_video(filename):
response = send_from_directory(DOWNLOAD_DIR, filename, as_attachment=True)
response.headers["Access-Control-Allow-Origin"] = "*"
return response
if __name__ == '__main__':
app.run()
File 3: passenger_wsgi.py (The Entry Point)
Python
from app import app as application
🎨 PHASE 2: The Frontend (The Interface)
Now we create the standalone tool page. You have two options: a direct integration into a Laravel/PHP site, or a WordPress plugin.
Option A: Laravel / Custom PHP
This creates a "Nuclear Clean" page with no header/footer to distract the user.
1. Create the Route (routes/web.php):
PHP
Route::view('/tiktok-downloader', 'tiktok-video-downloader');
2. Create the View (resources/views/tiktok-video-downloader.blade.php): Note: Replace https://api.yourdomain.com in the fetch request with your actual API subdomain.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TikTok Video Downloader - No Watermark</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
/* Nuclear Clean Styles */
body { font-family: 'Inter', sans-serif; background: #f8fafc; color: #0f172a; display: flex; flex-direction: column; min-height: 100vh; margin: 0; }
.main-wrapper { max-width: 800px; margin: 60px auto; padding: 0 20px; width: 100%; }
.tool-card { background: #fff; padding: 50px 30px; border-radius: 24px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); text-align: center; border: 1px solid #e2e8f0; }
.tool-icon { font-size: 50px; margin-bottom: 20px; }
h1 { font-size: 2.2rem; font-weight: 800; margin-bottom: 10px; }
.subtitle { color: #64748b; margin-bottom: 40px; }
.input-group { position: relative; max-width: 600px; margin: 0 auto; }
.url-input { width: 100%; padding: 18px 25px; border-radius: 50px; border: 2px solid #e2e8f0; font-size: 1rem; outline: none; box-sizing: border-box; }
.url-input:focus { border-color: #2563eb; }
.dl-btn { position: absolute; right: 6px; top: 6px; bottom: 6px; background: #2563eb; color: white; border: none; padding: 0 30px; border-radius: 40px; font-weight: 600; cursor: pointer; }
.dl-btn:hover { background: #1d4ed8; }
#result-area { display: none; margin-top: 40px; border-top: 1px solid #eee; padding-top: 30px; animation: slideUp 0.5s ease; }
.result-box { display: flex; align-items: center; gap: 20px; text-align: left; background: #f1f5f9; padding: 20px; border-radius: 16px; }
.thumb { width: 100px; height: 100px; border-radius: 12px; object-fit: cover; background: #000; }
.save-btn { display: inline-block; background: #10b981; color: white; padding: 10px 20px; border-radius: 8px; text-decoration: none; font-weight: 600; margin-top: 8px; }
@keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 600px) { .result-box { flex-direction: column; text-align: center; } .dl-btn { position: static; width: 100%; margin-top: 10px; } }
</style>
</head>
<body>
<div class="main-wrapper">
<div class="tool-card">
<div class="tool-icon"><i class="fab fa-tiktok"></i></div>
<h1>TikTok Downloader</h1>
<p class="subtitle">Download HD videos without watermark.</p>
<div class="input-group">
<input type="text" id="tkUrl" class="url-input" placeholder="Paste link here..." autocomplete="off">
<button id="dlBtn" class="dl-btn" onclick="process()">Download</button>
</div>
<div id="loader" style="display:none; margin-top:20px; color:#2563eb; font-weight:600;">Processing...</div>
<div id="errorBox" style="display:none; margin-top:20px; color:red;"></div>
<div id="result-area">
<div class="result-box">
<img id="vidThumb" class="thumb" src="">
<div>
<h3 id="vidTitle" style="font-size:1.1rem; margin-bottom:5px;">Video Title</h3>
<p style="font-size:0.9rem; color:#64748b;">No Watermark • <span id="fileSize"></span></p>
<a id="finalLink" href="#" class="save-btn">Save Video</a>
</div>
</div>
</div>
</div>
</div>
<script>
async function process() {
const url = document.getElementById('tkUrl').value.trim();
const btn = document.getElementById('dlBtn');
const loader = document.getElementById('loader');
const error = document.getElementById('errorBox');
const result = document.getElementById('result-area');
if (!url) { alert("Paste a link!"); return; }
btn.disabled = true; btn.innerText = "Wait...";
loader.style.display = 'block'; error.style.display = 'none'; result.style.display = 'none';
try {
// UPDATE THIS URL:
const response = await fetch('https://api.yourdomain.com/get-link/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url, format_type: 'mp4' })
});
const data = await response.json();
if (data.success) {
document.getElementById('vidTitle').innerText = data.title;
document.getElementById('vidThumb').src = data.thumbnail;
document.getElementById('fileSize').innerText = data.size_mb + " MB";
document.getElementById('finalLink').href = data.download_url;
result.style.display = 'block';
} else { throw new Error(data.error); }
} catch (e) {
error.innerText = e.message; error.style.display = 'block';
} finally {
btn.disabled = false; btn.innerText = "Download"; loader.style.display = 'none';
}
}
</script>
</body>
</html>
Option B: WordPress Shortcode
If you are using WordPress, you can create a simple plugin to use the [tiktok_downloader] shortcode.
- Download the WPCode plugin (or any Code Snippets plugin).
- Create a new PHP Snippet.
- Paste the following code (remember to update the API URL in the Javascript section at the bottom):
PHP
/*
* Plugin Name: TikTok Downloader Tool
* Description: A shortcode [tiktok_downloader] to display the TikTok Video Downloader.
* Version: 1.0
*/
function tiktok_downloader_shortcode() {
ob_start();
?>
<style>
/* Scoped Styles */
#tk-tool-root { font-family: 'Inter', sans-serif; background: #f8fafc; color: #0f172a; padding: 20px; border-radius: 12px; max-width: 900px; margin: 20px auto; }
.rating-widget, .sticky-rating, #rating-popup { display: none !important; opacity: 0 !important; pointer-events: none !important; }
#tk-tool-root .tool-card { background: #ffffff; border-radius: 24px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; text-align: center; padding: 40px 20px; }
#tk-tool-root .tool-icon { font-size: 50px; margin-bottom: 20px; color: #000; }
#tk-tool-root h2 { font-size: 2rem; font-weight: 800; color: #0f172a; margin-bottom: 10px; }
#tk-tool-root .subtitle { font-size: 1.1rem; color: #64748b; margin-bottom: 30px; }
#tk-tool-root .input-group { position: relative; max-width: 600px; margin: 0 auto; }
#tk-tool-root .url-input { width: 100%; padding: 18px 140px 18px 25px; font-size: 1rem; border: 2px solid #e2e8f0; border-radius: 50px; outline: none; background: #f8fafc; color: #334155; }
#tk-tool-root .url-input:focus { border-color: #2563eb; background: #fff; }
#tk-tool-root .download-btn { position: absolute; right: 6px; top: 6px; bottom: 6px; background: #2563eb; color: white; border: none; padding: 0 25px; border-radius: 40px; font-weight: 700; cursor: pointer; }
#tk-tool-root .download-btn:hover { background: #1d4ed8; }
#tk-loader { display: none; margin-top: 25px; color: #2563eb; font-weight: 600; }
#tk-errorBox { display: none; margin-top: 20px; padding: 15px; background: #fef2f2; color: #ef4444; border-radius: 12px; }
#tk-result-area { display: none; margin-top: 40px; padding-top: 30px; border-top: 1px solid #f1f5f9; }
#tk-tool-root .result-box { display: flex; align-items: center; background: #f8fafc; padding: 20px; border-radius: 20px; border: 1px solid #e2e8f0; text-align: left; gap: 20px; max-width: 550px; margin: 0 auto; }
#tk-tool-root .thumb-img { width: 100px; height: 100px; border-radius: 12px; object-fit: cover; background: #ddd; }
#tk-tool-root .save-btn { display: inline-block; background: #10b981; color: white; padding: 8px 18px; border-radius: 8px; font-weight: 600; margin-top: 8px; text-decoration: none; }
@media (max-width: 600px) { #tk-tool-root .url-input { padding: 15px; padding-bottom: 60px; text-align: center; } #tk-tool-root .download-btn { position: absolute; width: calc(100% - 12px); bottom: 6px; right: 6px; left: 6px; top: auto; } #tk-tool-root .result-box { flex-direction: column; text-align: center; } }
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<div id="tk-tool-root">
<div class="tool-card">
<div class="tool-icon"><i class="fab fa-tiktok"></i></div>
<h2>TikTok Video Downloader</h2>
<p class="subtitle">Save videos in HD without watermark.</p>
<div class="input-group">
<input type="text" id="tkUrl" class="url-input" placeholder="Paste video link..." autocomplete="off">
<button id="dlBtn" class="download-btn" onclick="processTikTok()">Download</button>
</div>
<div id="tk-loader"><i class="fas fa-circle-notch fa-spin"></i> Processing...</div>
<div id="tk-errorBox"></div>
<div id="tk-result-area">
<div class="result-box">
<img id="vidThumb" class="thumb-img" src="">
<div class="video-meta">
<h3 id="vidTitle">Video Title</h3>
<p style="color:#64748b; font-size: 0.85rem; margin-bottom: 5px;"><i class="fas fa-check-circle" style="color:#10b981"></i> No Watermark • <span id="fileSize"></span></p>
<a id="finalLink" href="#" class="save-btn" target="_blank">Save Video</a>
</div>
</div>
</div>
<p style="font-size: 0.85rem; color: #94a3b8; margin-top: 30px;">Disclaimer: For personal use only. Not affiliated with TikTok.</p>
</div>
</div>
<script>
async function processTikTok() {
const urlInput = document.getElementById('tkUrl');
const url = urlInput.value.trim();
const btn = document.getElementById('dlBtn');
const loader = document.getElementById('tk-loader');
const error = document.getElementById('tk-errorBox');
const result = document.getElementById('tk-result-area');
if (!url) { alert("Please paste a link!"); return; }
btn.disabled = true; btn.innerText = "Wait...";
loader.style.display = 'block'; error.style.display = 'none'; result.style.display = 'none';
try {
// UPDATE THIS URL:
const response = await fetch('https://api.yourdomain.com/get-link/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url, format_type: 'mp4' })
});
const data = await response.json();
if (data.success) {
document.getElementById('vidTitle').innerText = data.title;
document.getElementById('vidThumb').src = data.thumbnail;
document.getElementById('fileSize').innerText = data.size_mb + " MB";
document.getElementById('finalLink').href = data.download_url;
result.style.display = 'block';
} else { throw new Error(data.error || "Failed to fetch video."); }
} catch (e) {
error.innerText = e.message; error.style.display = 'block';
} finally {
btn.disabled = false; btn.innerText = "Download"; loader.style.display = 'none';
}
}
</script>
<?php
return ob_get_clean();
}
add_shortcode('tiktok_downloader', 'tiktok_downloader_shortcode');
✅ PHASE 3: Verification
- Restart App: Go to cPanel > Setup Python App and click Restart on your API domain.
- Test API: Visit https://api.yourdomain.com/. You should see {"status": "Online"}.
- Test Tool: Visit your tool page (e.g., yourdomain.com/tiktok-downloader), paste a video link, and click download.
Congratulations! You now have a fully functional, self-hosted video downloader running on your own infrastructure.

