Skip to content

Instantly share code, notes, and snippets.

@aabiji
Last active September 1, 2025 14:43
Show Gist options
  • Select an option

  • Save aabiji/c65254af440b1bb53149b2d6a9fafeca to your computer and use it in GitHub Desktop.

Select an option

Save aabiji/c65254af440b1bb53149b2d6a9fafeca to your computer and use it in GitHub Desktop.
Youtube video downloader
"""
A tiny youtube video downloader.
usage: yt.py [-h] [--only-audio] [--num_retries NUM_RETRIES] [--output OUTPUT] video_url
Inspried by this blog post: https://tyrrrz.me/blog/reverse-engineering-youtube-revisited
Note that the get_video_info function will be subject to change when Youtube changes their api.
Also note that ffmpeg and ffmpeg-python are required dependencies.
"""
import argparse
import ffmpeg
import os
import requests
import signal
import sys
import time
def format_size(size):
units = ["B", "KB", "MB", "GB", "TB"]
index = 0
while size > 1000:
size /= 1000
index += 1
return f"{round(size, 2)} {units[index]}"
def print_download_progress(bar_length, downloaded, total):
if downloaded != 0 and total != 0:
percentage = downloaded / total
else:
percentage = 0
amount = percentage * bar_length
progress = ("=" * int(amount)).ljust(bar_length - 1)
sys.stdout.write("\033[K")
message = f"[{progress}] {format_size(downloaded)}/{format_size(total)}"
print(message, end="\r")
def download_byte_range(filename, url):
chunk_size = 1024 * 100
total = int(requests.head(url).headers["Content-Length"])
amount_read = 0
try:
# Continue from where we stopped
if os.path.exists(filename):
filesize = os.path.getsize(filename)
if filesize >= total:
print("Already downloaded")
return True
headers = {"Range": f"bytes={filesize}-"}
amount_read = filesize
else:
headers = {}
with requests.get(url, headers=headers, stream=True) as req:
req.raise_for_status()
mode = "wb" if headers == {} else "ab"
with open(filename, mode) as file:
for chunk in req.iter_content(chunk_size=chunk_size):
file.write(chunk)
amount_read += sys.getsizeof(chunk)
print_download_progress(30, amount_read, total)
return True
except requests.exceptions.RequestException as e:
print(f"Download error: {e}")
return False
def download_bytes(filename, url, max_retries):
retries = 0
while retries < max_retries:
success = download_byte_range(filename, url)
print()
if not success:
print(f"Retrying {retries}/{max_retries}")
retries += 1
else:
break
time.sleep(0.25)
if retries == max_retries:
print("Max retries exceeded. Stopping")
exit()
def get_video_info(video_id):
key = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w"
endpoint = f"https://www.youtube.com/youtubei/v1/player?key={key}"
payload = {
"videoId": video_id,
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "20.34.37",
}
},
}
response = requests.post(endpoint, json=payload)
if response.status_code != 200:
print("Error getting video info.")
exit()
return response.json()
def get_best_stream(streams, searchingAudio):
filtered = []
for stream in streams:
mimetype = stream["mimeType"]
if searchingAudio and "audio" in mimetype:
filtered.append(stream)
elif not searchingAudio and "video" in mimetype:
filtered.append(stream)
# The higher the bitrate the better the stream quality
def bitrates(s):
return int(s["averageBitrate"])
return max(filtered, key=bitrates)
def download_stream(streams, title, is_audio, max_retries):
stream = get_best_stream(streams, is_audio)
container = stream["mimeType"].split(";")[0][6:]
stream_type = "audio" if is_audio else "video"
url = stream["url"]
file = f"{title} -- {stream_type}.{container}"
download_bytes(file, url, max_retries)
return file
def merge_streams(video_file, audio_file, output_file, only_audio):
if not only_audio:
vstream = ffmpeg.input(video_file)
astream = ffmpeg.input(audio_file)
stream = ffmpeg.output(vstream, astream, output_file, loglevel="quiet")
ffmpeg.run(stream, overwrite_output=True)
os.remove(video_file)
os.remove(audio_file)
else:
astream = ffmpeg.input(audio_file)
stream = ffmpeg.output(astream, output_file, loglevel="quiet")
ffmpeg.run(stream, overwrite_output=True)
os.remove(audio_file)
def download_video(video_id, only_audio, max_retries, output_file):
info = get_video_info(video_id)
title = info["videoDetails"]["title"]
print(f"Downloading '{title}'")
video_file = ""
audio_file = ""
streams = info["streamingData"]["adaptiveFormats"]
if output_file is None:
output_file = f"{title}.{'mp3' if only_audio else 'mp4'}"
if not only_audio:
print("Downloading video ...")
video_file = download_stream(streams, title, False, max_retries)
print("Downloading audio ...")
audio_file = download_stream(streams, title, True, max_retries)
merge_streams(video_file, audio_file, output_file, only_audio)
def signal_handler(signal, frame):
print("Exiting ...")
exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
parser = argparse.ArgumentParser(description="Youtube video downloader")
parser.add_argument("video_url", type=str, help="Url to the youtube video")
parser.add_argument("--only-audio", action="store_true", help="Only download audio")
parser.add_argument("--num_retries", type=int, help="Set the number of retries.")
parser.add_argument("--output", type=str, help="The file to download to")
args = parser.parse_args()
if args.num_retries is None:
args.num_retries = 3
id_index = args.video_url.find("v=")
if id_index == -1:
print("Please supply a valid youtube video url.")
exit()
video_id = args.video_url[id_index + 2 : id_index + 13]
download_video(video_id, args.only_audio, args.num_retries, args.output)
@3052
Copy link

3052 commented Aug 31, 2025

does not work any more

"playabilityStatus": {
    "status": "UNPLAYABLE",
    "reason": "This video is not available"
},

@aabiji
Copy link
Author

aabiji commented Sep 1, 2025

Youtube changed their apis again. Every other client I fiddled either doesn't provide urls, or requires authentication to use those urls. In order to bypass that I'd need to reverse engineer Youtube's obsfucation, like a game of whack-a-mole.

However, there are projects youtube-dl and yt-dlp that are commited to doing that. So, it'd be better to use them instead. Thanks for the feedback.

@3052
Copy link

3052 commented Sep 1, 2025

these are OK

200 OK {Status:OK Reason:} &{18 ANDROID_KIDS OK 7.36.1 QmpHcrA2hC0}
200 OK {Status:OK Reason:} &{19 IOS_KIDS OK 7.36.1 QmpHcrA2hC0}
200 OK {Status:OK Reason:} &{28 ANDROID_VR OK 1.37 fD0qZRK1lQ0}
200 OK {Status:OK Reason:} &{3 ANDROID OK 20.34.37 QmpHcrA2hC0}

@aabiji
Copy link
Author

aabiji commented Sep 1, 2025

Hey you're right! The ANDROID client, version 20.34.37 works. Thanks for pointing that out. Patched the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment