
Python Scripts to Download Your Videos & Subtitles Overnight
Batch-download authorized videos and subtitles with yt-dlp; wake up to organized assets.
Queue it, sleep, wake up to clean folders. Use this only for content you own, have explicit permission to download, or Creative Commons works that permit reuse. Respect platform Terms of Service and licenses.

What problem this solves (with real use cases)
- Batch-download overnight. Queue a playlist (your own/authorized/CC) and wake up to organized files.
- Stay organized automatically. Names, dates, and folders are consistent—no more
final_final(2).mp4
. - Capture subtitles for editing/search. Pull publisher subs or auto-captions when available.
- Be a good citizen. Add polite delays, limit retries, and only download content you’re allowed to use.
Install the tools (macOS & Windows)
You need two things: Python and yt‑dlp (plus FFmpeg for muxing/subtitles).
macOS (Homebrew)
Install Homebrew if you don’t have it:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Install Python and tools:
brew install python yt-dlp ffmpeg
Confirm versions:
python3 --version
yt-dlp --version
ffmpeg -version
Windows (Chocolatey)
Run PowerShell as Administrator, then install Chocolatey:
Set-ExecutionPolicy Bypass -Scope Process -Force; `
[System.Net.ServicePointManager]::SecurityProtocol = `
[System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
Install Python and tools:
choco install python yt-dlp ffmpeg -y
Confirm:
python --version
yt-dlp --version
ffmpeg -version
If
ffmpeg
isn’t found, close and reopen the terminal soPATH
updates.
Choose a safe folder structure
Keep things predictable so your editor/scripts can find files every time.
/ingest
/YYYYMMDD/ # date you downloaded
/<channel_or_playlist>/
video_title_<id>.mp4
video_title_<id>.<lang>.srt
video_title_<id>.info.json # optional metadata
Examples
/ingest/20250914/NASA/spacewalk_abc123.mp4
/ingest/20250914/MyChannel/how_to_edit_efg456.en.srt

yt‑dlp basics in plain English
- Formats:
-f mp4
tries to get mp4; or let yt‑dlp pick with-f bestvideo*+bestaudio/best
. - Output templates:
-o
controls names/folders using fields like%(uploader)s
,%(title)s
,%(id)s
,%(upload_date)s
. - Subtitles:
--write-sub
(publisher) and--write-auto-sub
(auto) with--sub-lang "en.*"
and--convert-subs srt
. - Respectful pacing:
--sleep-requests 2 --max-sleep-interval 5 --retries 10
. - Avoid duplicates:
--download-archive archive.txt
prevents re-downloading the same IDs. - Skip errors:
-i
ignores individual failures and keeps going. - Sidecar metadata (optional):
--write-info-json
.
Copy‑paste examples
Download a whole (authorized/CC) playlist into dated folders
macOS/Linux
yt-dlp -ciw -f mp4 --write-sub --write-auto-sub --sub-lang "en.*,th.*" --convert-subs srt --download-archive archive.txt --sleep-requests 2 --max-sleep-interval 5 --retries 10 --paths "base=./ingest" -o "%(upload_date)s/%(uploader)s/%(title).80s_%(id)s.%(ext)s" "https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID"
Windows (PowerShell)
yt-dlp -ciw `
-f mp4 `
--write-sub --write-auto-sub --sub-lang "en.*,th.*" --convert-subs srt `
--download-archive archive.txt `
--sleep-requests 2 --max-sleep-interval 5 --retries 10 `
--paths "base=.\ingest" `
-o "%(upload_date)s/%(uploader)s/%(title).80s_%(id)s.%(ext)s" `
"https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID"
Download from a text file of URLs (one per line)
macOS/Linux
yt-dlp -ciw -f mp4 --write-auto-sub --convert-subs srt --download-archive archive.txt --paths "base=./ingest" -o "%(upload_date)s/%(uploader)s/%(title).80s_%(id)s.%(ext)s" -a urls.txt
Windows
yt-dlp -ciw -f mp4 --write-auto-sub --convert-subs srt `
--download-archive archive.txt `
--paths "base=.\ingest" `
-o "%(upload_date)s/%(uploader)s/%(title).80s_%(id)s.%(ext)s" `
-a urls.txt
Notes:
-c
continues partial downloads,-i
ignores errors,-w
avoids overwrites.%(upload_date)s
isYYYYMMDD
.
The simplest Python wrapper
A minimal script that reads your URLs, calls yt‑dlp, and writes a manifest CSV so you can track successes/failures.
- Put your playlist or video links in
urls.txt
(one per line). - Save this as
overnight_dl.py
next tourls.txt
:
#!/usr/bin/env python3
import csv, subprocess, time, pathlib, datetime, sys
BASE = pathlib.Path(__file__).resolve().parent
INGEST = BASE / "ingest"
ARCHIVE = BASE / "archive.txt"
URLS_FILE = BASE / "urls.txt"
LOG_FILE = BASE / "overnight.log"
MANIFEST = BASE / "manifest.csv"
CMD = [
"yt-dlp",
"-ciw",
"-f", "mp4",
"--write-sub", "--write-auto-sub", "--sub-lang", "en.*,th.*",
"--convert-subs", "srt",
"--download-archive", str(ARCHIVE),
"--sleep-requests", "2", "--max-sleep-interval", "5", "--retries", "10",
"--paths", f"base={INGEST}",
"-o", "%(upload_date)s/%(uploader)s/%(title).80s_%(id)s.%(ext)s",
]
def run(url: str) -> int:
with LOG_FILE.open("a", encoding="utf-8") as log:
log.write(f"\n--- {datetime.datetime.now().isoformat()} START {url}\n")
p = subprocess.run(CMD + [url], stdout=log, stderr=log, text=True)
log.write(f"--- {datetime.datetime.now().isoformat()} END {url} rc={p.returncode}\n")
return p.returncode
def main():
INGEST.mkdir(parents=True, exist_ok=True)
ARCHIVE.touch(exist_ok=True)
if not URLS_FILE.exists():
print("No urls.txt found. Create one with your own/authorized/CC URLs.", file=sys.stderr)
sys.exit(1)
rows = []
for url in URLS_FILE.read_text(encoding="utf-8").splitlines():
url = url.strip()
if not url or url.startswith("#"):
continue
rc = run(url)
rows.append({
"timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
"url": url,
"ok": "yes" if rc == 0 else "no"
})
time.sleep(1) # small pause between items
# Append to (or create) manifest
write_header = not MANIFEST.exists()
with MANIFEST.open("a", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["timestamp", "url", "ok"])
if write_header:
w.writeheader()
w.writerows(rows)
if __name__ == "__main__":
main()
- Run it
macOS
python3 overnight_dl.py
Windows
python overnight_dl.py
What to expect
- New files appear under
ingest/YYYYMMDD/<uploader>/...
manifest.csv
tells you what succeeded/failed.overnight.log
contains detailed output for debugging.archive.txt
prevents re-downloading the same video next time.

Scheduling overnight
macOS (cron)
Find full paths:
which python3
pwd
Assume:
- Python is
/opt/homebrew/bin/python3
(or/usr/bin/python3
) - Project folder is
/Users/you/OvernightDL
Edit your crontab:
crontab -e
Run every night at 2:00 AM:
0 2 * * * PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" /opt/homebrew/bin/python3 /Users/you/OvernightDL/overnight_dl.py >> /Users/you/OvernightDL/overnight.log 2>&1
Keep your Mac awake: System Settings → Battery → prevent sleep during scheduled runs.
Windows (Task Scheduler)
- Task Scheduler → Create Basic Task…
- Trigger: Daily at 2:00 AM
- Action: Start a program
- Program/script: path to
python.exe
(e.g.,C:\Users\you\AppData\Local\Programs\Python\Python312\python.exe
) - Add arguments:
C:\path\to\overnight_dl.py
- Start in:
C:\path\to\your\project\folder
(important)
- Program/script: path to
- Properties → Run whether user is logged on or not; set OS version; allow on battery if needed.
- Check the History tab and
overnight.log
next day.
Subtitles & fallbacks
--write-sub
grabs publisher-supplied subs (when available).--write-auto-sub
grabs auto-captions when allowed.--sub-lang "en.*,th.*"
fetches language variants;--convert-subs srt
outputs.srt
.- If no subs exist, the download still completes; you just won’t see an
.srt
file.
Good practice
- Keep a
subs/
subfolder if you prefer to separate captions (use--paths subtitles=./subs
). - If you publish, follow license/attribution rules for CC content and ensure you have rights to remix/use the assets you downloaded.
Safety, ethics, and guardrails
- Only download what you’re allowed to. Your own uploads; explicit client permissions; or CC works that permit reuse/monetization (e.g., CC BY). If unsure, don’t download.
- Respect Terms of Service. Use tools in ways that comply with site rules and laws.
- Be a good internet citizen. Add light pacing:
--sleep-requests 2 --max-sleep-interval 5 --retries 10
. Keep concurrency low and run big jobs overnight. - Keep proof for CC/licensed assets. Save source URL, license link, and a dated screenshot showing license/version. Log in a Reuse Ledger with exact attribution text for your description.
- Don’t redistribute third‑party files. Remix only as licenses allow; don’t re-upload raw assets you don’t own.
- Handle personal data carefully. If you ingest interviews/PII, store locally with good hygiene and backups. Encrypt if needed.
- Have a swap plan. Keep safe b-roll/music on hand to replace anything that triggers a claim.
Troubleshooting for beginners
“command not found” / 'yt-dlp' is not recognized
Install via Homebrew/Chocolatey, then reopen the terminal. Check: yt-dlp --version
, ffmpeg -version
, python --version
.
“Permission denied” (macOS) / script won’t run
Make scripts executable: chmod +x script.sh
. For Python: python3 file.py
.
PowerShell blocked scripts (Windows)
Allow local scripts (once):
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Rate limits / timeouts
Add waits and retries:
--sleep-requests 2 --max-sleep-interval 5 --retries 10 -i
Subtitles didn’t appear
Not all videos have subs. Request both publisher and auto; if none exist, note “no-subs” in your manifest for later transcription.
Post‑processing error: ffprobe/ffmpeg not found
Install FFmpeg (brew install ffmpeg
or choco install ffmpeg -y
) and reopen your terminal.
Filenames too long (Windows)
Use shorter templates and safe characters:
- Add
--restrict-filenames
- Limit title length:
%(title).60s
- Flatten folders for the run
- As a last resort, download to a short path like
C:\ingest
Weird paths or spaces in names
Wrap paths in quotes: "C:\My Videos\ingest"
or "My Clip.mp4"
.
Still stuck?
Open overnight.log
, copy the exact error line, and search it with “yt-dlp” or “ffmpeg”. Most issues have a known fix.
Optional: “enhanced” manifest (more fields)
If you also write info JSON sidecars (--write-info-json
), you can extend the script later to parse fields like title, id, uploader, upload_date, duration, and subtitle languages into your CSV. This helps with analytics and edit prep.
Manifest headers idea
timestamp,url,id,uploader,upload_date,title,duration,has_subs,languages,ok

FAQ
Can I download private or paid content?
Only if it’s yours and allowed by the platform and license/contract. This guide assumes public videos you own/are authorized to download or CC‑permitted works.
How do I limit quality/size (e.g., 1080p)?
Use format selection:
-f "bv*[height<=1080]+ba/b[height<=1080]"
Can I resume if the connection drops?
Yes — -c
continues partial files; -w
avoids overwrites; --download-archive
skips repeats.
How do I pick subtitle languages?
--sub-lang "en.*,th.*"
grabs English variants and Thai. Add --convert-subs srt
for editor-friendly SRT.
Is auto‑captioning reliable?
Varies. Treat auto subs as a starting point; review or re‑transcribe important videos later.
Can I run multiple downloads at once?
Possible, but for beginners use a single queue with light sleeps to avoid throttling.