#!/usr/bin/env bash set -e TARGET="$HOME/yt-download" VENV="$TARGET/env" # 1) Create project dir mkdir -p "$TARGET" cd "$TARGET" # 2) Create venv if missing if [ ! -d "$VENV" ]; then python3 -m venv env fi # 3) Activate venv # shellcheck disable=SC1090 source "$VENV/bin/activate" # 4) Install Python deps quietly pip install --upgrade pip -q pip install pytubefix tqdm -q # 5) Check Homebrew if ! command -v brew >/dev/null 2>&1; then cat </dev/null 2>&1; then brew install ffmpeg >/dev/null 2>&1 fi # 7) Write the Python downloader into downloader.py cat > downloader.py << 'PYCODE' #!/usr/bin/env python3 import os import sys import subprocess import time from urllib.parse import urlparse from pytubefix import YouTube from tqdm import tqdm # ─── Unicode fractional bar setup ────────────────────────────────────────────── BLOCKS = ["","ā–","ā–Ž","ā–","ā–Œ","ā–‹","ā–Š","ā–‰"] FULL_BLOCK = "ā–ˆ" BAR_LEN = 30 def make_bar(ratio): total_eights = int(ratio * BAR_LEN * 8) full_chars = total_eights // 8 frac = total_eights % 8 bar = FULL_BLOCK*full_chars + (BLOCKS[frac] if frac else "") bar += " "*(BAR_LEN - full_chars - (1 if frac else 0)) return bar def progress_cb(label, start): def cb(stream, chunk, rem): total = stream.filesize done = total - rem now = time.time() elapsed = now - start speed = (done/(1024*1024))/max(elapsed,1e-6) eta = (total-done)/(speed*1024*1024) if speed>0 else 0 mb_done = done/(1024*1024) mb_tot = total/(1024*1024) pct = done/total bar = make_bar(pct) line = ( f"\r{label}: [{bar}] {int(pct*100):3d}% " f"{mb_done:5.1f}/{mb_tot:5.1f} MB " f"{speed:6.2f} MB/s " f"{int(eta):4d}s left " f"{int(elapsed):4d}s elapsed" ) sys.stdout.write(line) sys.stdout.flush() return cb def download_stream(url, label, filters, order, temp_dir, fn): start = time.time() cb = progress_cb(label, start) try: yt = YouTube(url, on_progress_callback=cb) except Exception: print("\nError: Video not found. Is it private? URLs are case-sensitive.", file=sys.stderr) sys.exit(1) stream = yt.streams.filter(**filters).order_by(order).desc().first() if not stream: print("\nError: Video not found. Is it private? URLs are case-sensitive.", file=sys.stderr) sys.exit(1) stream.download(output_path=temp_dir, filename=fn) print() def main(): url = sys.argv[1] if len(sys.argv)>1 else input("Input Video URL: ").strip() if not url: print("Error: Please enter a URL.", file=sys.stderr) sys.exit(1) parsed = urlparse(url) if not parsed.scheme or not parsed.netloc: print("Error: That's not a valid URL.", file=sys.stderr) sys.exit(1) if 'youtube.com' not in parsed.netloc and 'youtu.be' not in parsed.netloc: print("Error: That's not a YouTube URL.", file=sys.stderr) sys.exit(1) temp_dir = os.path.expanduser("~/yt-download") final_dir = os.path.expanduser("~/Downloads") os.makedirs(temp_dir, exist_ok=True) os.makedirs(final_dir, exist_ok=True) # Download video download_stream( url, "Video", {"progressive":False,"file_extension":"mp4","type":"video"}, "resolution", temp_dir, "video_temp.mp4" ) # Download audio download_stream( url, "Audio", {"only_audio":True,"file_extension":"mp4"}, "abr", temp_dir, "audio_temp.mp4" ) print("Creating") yt = YouTube(url) title = yt.title.replace("/","_").replace("\\","_") + ".mp4" final = os.path.join(final_dir, title) subprocess.run([ "ffmpeg","-y", "-i",os.path.join(temp_dir,"video_temp.mp4"), "-i",os.path.join(temp_dir,"audio_temp.mp4"), "-c","copy", final ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) os.remove(os.path.join(temp_dir,"video_temp.mp4")) os.remove(os.path.join(temp_dir,"audio_temp.mp4")) print(f"\nāœ… Download and merge complete! Saved to: {final}") if __name__=="__main__": main() PYCODE chmod +x downloader.py # 8) Run downloader ./downloader.py "$@" # 9) Deactivate venv deactivate || true