Last active
February 26, 2026 15:02
-
-
Save tiagocesar/b5d98d086b534ddddc9f4a2a8dba8285 to your computer and use it in GitHub Desktop.
Download videos from your Watch Later playlist on YouTube
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ============================================================================== | |
| # Setup Instructions: | |
| # | |
| # 1. Dependencies: | |
| # Install the required packages using Homebrew: | |
| # brew install yt-dlp ffmpeg | |
| # | |
| # 2. Folder Structure: | |
| # This script expects to be run from inside a 'scripts' directory, which | |
| # should be a subdirectory of your main video download folder. | |
| # | |
| # Example structure: | |
| # /Users/YourUser/Media/Videos/ | |
| # ├── scripts/ | |
| # │ ├── yt_download.sh | |
| # │ ├── archive.txt (will be created here by yt-dlp) | |
| # │ └── yt.log (will be created here by the script) | |
| # └── <downloaded_video.mp4> (videos will be saved in the parent directory) | |
| # 3. Browser: | |
| # We use Firefox because yt-dlp needs access to your browser cookies so it can | |
| # see the videos on your private playlist. If you use any Chrome based browser, | |
| # cron won't be able to get your cookies and the script will fail. Using Firefox | |
| # is the easiest way to circumvent this (be sure to open Firefox and log in to your | |
| # YouTube account). | |
| # ============================================================================== | |
| # Export the PATH so Cron can find Node.js, Python, and ffmpeg | |
| export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" | |
| # Always run from the script's own directory so relative paths | |
| # (yt.log, archive.txt, output path) resolve correctly under Cron | |
| cd "$(dirname "$0")" || exit 1 | |
| # --- Lockfile mechanism (prevents concurrent runs) --- | |
| LOCKDIR="$(dirname "$0")/.yt_download.lock" | |
| STALE_THRESHOLD=7200 # 2 hours in seconds – auto-remove stale locks | |
| # Remove stale lock (self-heal if a previous run crashed) | |
| if [ -d "$LOCKDIR" ]; then | |
| lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKDIR") )) | |
| if [ "$lock_age" -gt "$STALE_THRESHOLD" ]; then | |
| echo "$(date '+%Y-%m-%d %H:%M:%S') - Removing stale lock (age: ${lock_age}s)" >> yt.log | |
| rm -rf "$LOCKDIR" | |
| fi | |
| fi | |
| # Try to acquire lock (mkdir is atomic on all filesystems) | |
| if ! mkdir "$LOCKDIR" 2>/dev/null; then | |
| echo "$(date '+%Y-%m-%d %H:%M:%S') - Another instance is already running, exiting." >> yt.log | |
| exit 0 | |
| fi | |
| # Ensure lock is released on exit (normal, error, or signal) | |
| trap 'rm -rf "$LOCKDIR"' EXIT INT TERM HUP | |
| # --- End lockfile mechanism --- | |
| # Path to yt-dlp (Use absolute path just to be safe for Cron) | |
| YTDLP="/opt/homebrew/bin/yt-dlp" | |
| # Check if yt-dlp exists there, otherwise try standard path | |
| if [ ! -f "$YTDLP" ]; then | |
| YTDLP="/usr/local/bin/yt-dlp" | |
| fi | |
| LOGFILE="yt.log" | |
| TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" | |
| echo "" >> "$LOGFILE" | |
| echo "=== Run started: $TIMESTAMP ===" >> "$LOGFILE" | |
| $YTDLP --cookies-from-browser firefox \ | |
| --download-archive archive.txt \ | |
| --merge-output-format mp4 \ | |
| -f "bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[ext=mp4]/best" \ | |
| --embed-thumbnail \ | |
| --embed-metadata \ | |
| --sub-langs "en.*" \ | |
| --convert-subs srt \ | |
| --embed-subs \ | |
| --newline \ | |
| --progress \ | |
| --print before_dl:"%(id)s: %(title)s" \ | |
| -o "../$(date +%Y%m%d) - %(title)s.%(ext)s" \ | |
| "https://www.youtube.com/playlist?list=WL" 2>&1 | awk 'BEGIN { last_bucket = -1 } { | |
| if ($0 ~ /\[download\]/ && $0 ~ /[0-9]%/) { | |
| for (i=1; i<=NF; i++) { | |
| if ($i ~ /[0-9]%$/ || $i ~ /[0-9]%[^a-zA-Z]/) { | |
| val = $i | |
| gsub(/%/, "", val) | |
| pct = val + 0 | |
| if (pct < last_pct) last_bucket = -1 | |
| last_pct = pct | |
| bucket = int(pct / 10) | |
| if (bucket > last_bucket) { | |
| last_bucket = bucket | |
| print; fflush() | |
| } | |
| break | |
| } | |
| } | |
| } else { | |
| print; fflush() | |
| } | |
| }' >> "$LOGFILE" | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: yt-dlp exited with code $EXIT_CODE" >> "$LOGFILE" | |
| fi | |
| echo "=== Run finished: $(date '+%Y-%m-%d %H:%M:%S') (exit code: $EXIT_CODE) ===" >> "$LOGFILE" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is the cron command:
*/10 9-20 * * * /Users/YourUser/Path/To/scripts/yt_download.shAdjust the absolute path to where your script is. If unsure, run
pwdin the same folder and append the script name.To setup the cron job, run
crontab -e, edit the file with the line above, save, and exit.