Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 74 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
import uuid
import glob
import json
import re
import subprocess
import threading
import logging
from flask import Flask, request, jsonify, send_file, render_template

app = Flask(__name__)
DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), "downloads")
os.makedirs(DOWNLOAD_DIR, exist_ok=True)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Download timeout: 30 minutes (large files need more time)
DOWNLOAD_TIMEOUT = 1800

jobs = {}


Expand All @@ -19,6 +27,9 @@ def run_download(job_id, url, format_choice, format_id):

cmd = ["yt-dlp", "--no-playlist", "-o", out_template]

# Enable progress tracking via newline-separated output
cmd += ["--newline", "--progress"]

if format_choice == "audio":
cmd += ["-x", "--audio-format", "mp3"]
elif format_id:
Expand All @@ -29,10 +40,47 @@ def run_download(job_id, url, format_choice, format_id):
cmd.append(url)

try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0:
# Use Popen for real-time progress parsing
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)

progress_pattern = re.compile(r"\[download\]\s+Destination:|\[download\]\s+(\d+\.?\d*)%")
last_error_lines = []

for line in process.stdout:
line = line.strip()
if not line:
continue

# Track progress percentage
match = progress_pattern.search(line)
if match:
if match.group(1):
try:
pct = float(match.group(1))
job["progress"] = min(pct, 100.0)
except ValueError:
pass

# Track error/warning lines for better error reporting
if line.startswith("ERROR:") or line.startswith("WARNING:"):
last_error_lines.append(line)
logger.warning(f"Job {job_id}: {line}")

process.wait(timeout=DOWNLOAD_TIMEOUT)

if process.returncode != 0:
job["status"] = "error"
job["error"] = result.stderr.strip().split("\n")[-1]
# Use the most informative error line
if last_error_lines:
job["error"] = last_error_lines[-1].replace("ERROR: ", "")
else:
job["error"] = f"yt-dlp exited with code {process.returncode}"
return

files = glob.glob(os.path.join(DOWNLOAD_DIR, f"{job_id}.*"))
Expand All @@ -56,6 +104,7 @@ def run_download(job_id, url, format_choice, format_id):
pass

job["status"] = "done"
job["progress"] = 100.0
job["file"] = chosen
ext = os.path.splitext(chosen)[1]
title = job.get("title", "").strip()
Expand All @@ -65,12 +114,31 @@ def run_download(job_id, url, format_choice, format_id):
job["filename"] = f"{safe_title}{ext}" if safe_title else os.path.basename(chosen)
else:
job["filename"] = os.path.basename(chosen)

# Report file size for user awareness
try:
file_size = os.path.getsize(chosen)
job["file_size_mb"] = round(file_size / (1024 * 1024), 2)
except OSError:
pass

except subprocess.TimeoutExpired:
job["status"] = "error"
job["error"] = "Download timed out (5 min limit)"
job["error"] = f"Download timed out ({DOWNLOAD_TIMEOUT // 60} min limit). Try a lower quality format for large files."
# Clean up partial file
try:
process.kill()
except Exception:
pass
for f in glob.glob(os.path.join(DOWNLOAD_DIR, f"{job_id}.*")):
try:
os.remove(f)
except OSError:
pass
except Exception as e:
job["status"] = "error"
job["error"] = str(e)
logger.exception(f"Job {job_id} failed with unexpected error")


@app.route("/")
Expand Down Expand Up @@ -154,6 +222,8 @@ def check_status(job_id):
"status": job["status"],
"error": job.get("error"),
"filename": job.get("filename"),
"progress": job.get("progress"),
"file_size_mb": job.get("file_size_mb"),
})


Expand Down
25 changes: 24 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,21 @@
.card-status.done { color: var(--success); }
.card-status.error { color: var(--error); }

.progress-bar-wrap {
width: 100%;
height: 4px;
background: var(--card-border);
border-radius: 2px;
margin-top: 6px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 2px;
transition: width 0.3s ease;
}

.q-chip {
padding: 4px 10px;
border: 1.5px solid var(--card-border);
Expand Down Expand Up @@ -568,7 +583,12 @@ <h1>Re<em>Clip</em></h1>
if (c.status === 'ready') {
actionHtml = `<button class="card-dl-btn" onclick="dlCard(${idx})">Download</button>${qualityChips}`;
} else if (c.status === 'downloading') {
actionHtml = `<span class="card-status downloading"><span class="spin"></span> Downloading...</span>`;
const pct = c.progress != null ? Math.round(c.progress) : null;
const pctText = pct != null ? `${pct}%` : '';
actionHtml = `<span class="card-status downloading"><span class="spin"></span> Downloading... ${pctText}</span>`;
if (pct != null) {
actionHtml += `<div class="progress-bar-wrap"><div class="progress-bar-fill" style="width:${pct}%"></div></div>`;
}
} else if (c.status === 'done') {
actionHtml = `<button class="card-dl-btn done" onclick="saveCard(${idx})">Save</button>
<span class="card-status done">${esc(c.filename || '')}</span>`;
Expand Down Expand Up @@ -653,6 +673,9 @@ <h1>Re<em>Clip</em></h1>
c.status = 'error';
c.error = data.error;
renderCard(idx);
} else if (data.status === 'downloading' && data.progress != null) {
c.progress = data.progress;
renderCard(idx);
}
} catch {
clearInterval(iv);
Expand Down