refactor: improve error handling and response formatting in API endpoints
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

This commit is contained in:
Space-Banane
2026-04-06 23:09:06 +02:00
parent 002533044a
commit f34c90b999
2 changed files with 92 additions and 46 deletions

113
batch.py
View File

@@ -3,18 +3,25 @@ import json
from pathlib import Path from pathlib import Path
import os import os
COOKIES = "cookies.txt" # Get them using the browser extension "Get cookies.txt" and export them while logged in to your account. # Get cookies using the browser extension "Get cookies.txt" and export while
YTDLP = "yt-dlp.exe" # Change this if you aren't on Windows. Get the latest release from github # logged in to your account.
COOKIES = "cookies.txt"
# Change this if you aren't on Windows. Get the latest release from GitHub.
YTDLP = "yt-dlp.exe"
OUTPUT_DIR = Path(__file__).parent / "YOUR_OUTPUT_FOLDER" OUTPUT_DIR = Path(__file__).parent / "YOUR_OUTPUT_FOLDER"
# Input options: # Input options:
# - If `urls.json` exists in the project root it will be used. The JSON should be an array of strings (URLs) # - If `urls.json` exists in the project root it will be used.
# - Otherwise set URLs below or pass a custom JSON path via the `URLS_JSON` environment variable. # The JSON should be an array of strings (URLs).
# - Otherwise set URLs below or pass a custom JSON path via the
# `URLS_JSON` environment variable.
URLS = [ URLS = [
# fallback list — leave empty if you'll use urls.json # fallback list — leave empty if you'll use urls.json
] ]
URLS_JSON = os.environ.get("URLS_JSON", str(Path(__file__).parent / "urls.json")) default_urls = str(Path(__file__).parent / "urls.json")
URLS_JSON = os.environ.get("URLS_JSON", default_urls)
RESET = "\033[0m" RESET = "\033[0m"
BOLD = "\033[1m" BOLD = "\033[1m"
@@ -44,8 +51,13 @@ def fetch_info(url):
"generic:impersonate", "generic:impersonate",
url, url,
] ]
cwd_dir = Path(__file__).parent
result = subprocess.run( result = subprocess.run(
cmd, capture_output=True, text=True, timeout=30, cwd=Path(__file__).parent cmd,
capture_output=True,
text=True,
timeout=30,
cwd=cwd_dir,
) )
if result.returncode != 0: if result.returncode != 0:
return None, result.stderr.strip() return None, result.stderr.strip()
@@ -92,30 +104,40 @@ def main():
data = json.load(fh) data = json.load(fh)
if isinstance(data, list) and data: if isinstance(data, list) and data:
URLS.clear() URLS.clear()
URLS.extend([u for u in data if isinstance(u, str) and u.strip()]) for u in data:
if isinstance(u, str) and u.strip():
URLS.append(u)
total = len(URLS) total = len(URLS)
except Exception as e: except Exception as e:
log(f"Failed to load URLs from {URLS_JSON}: {e}", YELLOW) log(f"Failed to load URLs from {URLS_JSON}: {e}", YELLOW)
ok, skipped, failed = [], [], [] ok, skipped, failed = [], [], []
log(f"\n{''*55}", CYAN) log("\n" + "" * 55, CYAN)
log(f" yt-dlp batch — {total} posts → {OUTPUT_DIR.name}/", BOLD) log(" yt-dlp batch — {} posts → {}/".format(total, OUTPUT_DIR.name), BOLD)
log(f"{''*55}\n", CYAN) log("" + "" * 55 + "\n", CYAN)
for i, url in enumerate(URLS, 1): for i, url in enumerate(URLS, 1):
post_id = get_post_id(url) post_id = get_post_id(url)
log(f"[{i:02d}/{total}] {DIM}{url}{RESET}") header = "[{:02d}/{:d}]".format(i, total)
log(header + " " + DIM + url + RESET)
# Fetch metadata first to get the title # Fetch metadata first to get the title
log(f" fetching info…", DIM) log(" fetching info…", DIM)
info, err = fetch_info(url) info, err = fetch_info(url)
if info is None: if info is None:
# Likely a text post or unavailable # Likely a text post or unavailable
log( reason = err[:80] if err else "no video found"
f" {YELLOW}⚠ skipped — no media ({err[:80] if err else 'no video found'}){RESET}" msg = (
" "
+ YELLOW
+ "⚠ skipped — no media ("
+ reason
+ ")"
+ RESET
) )
log(msg)
skipped.append((url, err)) skipped.append((url, err))
print() print()
continue continue
@@ -125,24 +147,27 @@ def main():
out_dir = OUTPUT_DIR / folder_name out_dir = OUTPUT_DIR / folder_name
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
log(f" title: {BOLD}{title[:55]}{RESET}") log(" title: " + BOLD + title[:55] + RESET)
log(f" dir: {out_dir}") log(" dir: " + str(out_dir))
log(f" downloading…", DIM) log(" downloading…", DIM)
success, output = download(url, out_dir) success, output = download(url, out_dir)
if success: if success:
# Find what was downloaded # Find what was downloaded
files = list(out_dir.iterdir()) files = list(out_dir.iterdir())
sizes = [f"{f.stat().st_size / 1e6:.1f} MB" for f in files if f.is_file()] sizes = []
log( for f in files:
f" {GREEN}✓ done — {', '.join(sizes) if sizes else 'file saved'}{RESET}" if f.is_file():
) sizes.append("{:.1f} MB".format(f.stat().st_size / 1e6))
msg = ", ".join(sizes) if sizes else "file saved"
log(" " + GREEN + "✓ done — " + msg + RESET)
ok.append(url) ok.append(url)
else: else:
# Check if it's just no video (text post that slipped through info check) # Check if it's just no video
# (text post that slipped through info check)
if "no video" in output.lower() or "no formats" in output.lower(): if "no video" in output.lower() or "no formats" in output.lower():
log(f" {YELLOW}⚠ skipped — text post{RESET}") log(" " + YELLOW + "⚠ skipped — text post" + RESET)
skipped.append((url, "no video content")) skipped.append((url, "no video content"))
# Remove empty dir # Remove empty dir
try: try:
@@ -150,31 +175,43 @@ def main():
except OSError: except OSError:
pass pass
else: else:
log(f" {RED}✗ failed{RESET}") log(" " + RED + "✗ failed" + RESET)
# Print last few lines of output for context # Print last few lines of output for context
for line in output.splitlines()[-3:]: for line in output.splitlines()[-3:]:
log(f" {DIM}{line}{RESET}") log(" " + DIM + line + RESET)
failed.append( if output:
(url, output.splitlines()[-1] if output else "unknown error") last_err = output.splitlines()[-1]
) else:
last_err = "unknown error"
failed.append((url, last_err))
print() print()
# Summary # Summary
log(f"{''*55}", CYAN) log("" + "" * 55, CYAN)
log(f" Summary", BOLD) log(" Summary", BOLD)
log(f"{''*55}", CYAN) log("" + "" * 55, CYAN)
log(f" {GREEN}✓ downloaded: {len(ok)}{RESET}") log(" " + GREEN + "✓ downloaded: " + str(len(ok)) + RESET)
log(f" {YELLOW}⚠ skipped (text/no media): {len(skipped)}{RESET}") skipped_msg = (
log(f" {RED}✗ failed: {len(failed)}{RESET}") " " + YELLOW + "⚠ skipped (text/no media): "
+ str(len(skipped))
+ RESET
)
log(skipped_msg)
failed_msg = (
" " + RED + "✗ failed: "
+ str(len(failed))
+ RESET
)
log(failed_msg)
if failed: if failed:
log(f"\n Failed URLs:", RED) log("\n Failed URLs:", RED)
for url, reason in failed: for url, reason in failed:
log(f"{url}", RED) log("" + url, RED)
log(f" {DIM}{reason[:80]}{RESET}") log(" " + DIM + reason[:80] + RESET)
log(f"{''*55}\n", CYAN) log("" + "" * 55 + "\n", CYAN)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -71,15 +71,17 @@ def get_info():
cmd, capture_output=True, text=True, timeout=30, cwd=DOWNLOAD_DIR cmd, capture_output=True, text=True, timeout=30, cwd=DOWNLOAD_DIR
) )
if result.returncode != 0: if result.returncode != 0:
return jsonify({"error": result.stderr or "Failed to fetch info"}), 400 msg = result.stderr or "Failed to fetch info"
return jsonify({"error": msg}), 400
info = json.loads(result.stdout) info = json.loads(result.stdout)
formats = [] formats = []
for f in info.get("formats", []): for f in info.get("formats", []):
res = f.get("resolution") or f.get("format_note") or ""
formats.append( formats.append(
{ {
"id": f.get("format_id"), "id": f.get("format_id"),
"ext": f.get("ext"), "ext": f.get("ext"),
"resolution": f.get("resolution") or f.get("format_note") or "", "resolution": res,
"vcodec": f.get("vcodec", "none"), "vcodec": f.get("vcodec", "none"),
"acodec": f.get("acodec", "none"), "acodec": f.get("acodec", "none"),
"filesize": f.get("filesize") or f.get("filesize_approx"), "filesize": f.get("filesize") or f.get("filesize_approx"),
@@ -125,7 +127,11 @@ def start_download():
cmd.append(url) cmd.append(url)
job_id = str(uuid.uuid4()) job_id = str(uuid.uuid4())
thread = threading.Thread(target=run_ytdlp, args=(job_id, cmd), daemon=True) thread = threading.Thread(
target=run_ytdlp,
args=(job_id, cmd),
daemon=True,
)
thread.start() thread.start()
return jsonify({"job_id": job_id}) return jsonify({"job_id": job_id})
@@ -139,14 +145,17 @@ def job_status(job_id):
with jobs_lock: with jobs_lock:
job = jobs.get(job_id) job = jobs.get(job_id)
if not job: if not job:
yield f"data: {json.dumps({'error': 'Job not found'})}\n\n" payload = json.dumps({"error": "Job not found"})
yield "data: " + payload + "\n\n"
break break
lines = job["lines"] lines = job["lines"]
while sent < len(lines): while sent < len(lines):
yield f"data: {json.dumps({'line': lines[sent]})}\n\n" payload = json.dumps({"line": lines[sent]})
yield "data: " + payload + "\n\n"
sent += 1 sent += 1
if job["done"]: if job["done"]:
yield f"data: {json.dumps({'done': True, 'error': job['error']})}\n\n" payload = json.dumps({"done": True, "error": job["error"]})
yield "data: " + payload + "\n\n"
break break
time.sleep(0.2) time.sleep(0.2)
@@ -158,6 +167,6 @@ def job_status(job_id):
if __name__ == "__main__": if __name__ == "__main__":
print(f"[yt-dlp UI] Serving on http://localhost:5000") print("[yt-dlp UI] Serving on http://localhost:5000")
print(f"[yt-dlp UI] Download directory: {DOWNLOAD_DIR}") print("[yt-dlp UI] Download directory: {}".format(DOWNLOAD_DIR))
app.run(debug=False, host="0.0.0.0", port=5000, threaded=True) app.run(debug=False, host="0.0.0.0", port=5000, threaded=True)