Untitled

Anonymous
plain_text
09/26/2026 2:13 AM
12.6 KB
3
No Index
#!/usr/bin/env python3
"""
yt.py - A stripped-down YouTube video downloader (pure stdlib).

Distilled from yt-dlp (https://github.com/yt-dlp/yt-dlp) down to only the
parts needed to download a YouTube video. It mirrors this yt-dlp flow:

  1. Parse the video ID                    (extractor/youtube/_video.py)
  2. Grab INNERTUBE_API_KEY from the       (extractor/youtube/_base.py:
     watch page's ytcfg blob                 _download_ytcfg/_extract_ytcfg)
  3. POST to the Innertube `player` API    (extractor/youtube/_video.py:
     with a mobile client context            _extract_player_response)
  4. Read direct stream URLs from          (extractor/youtube/_video.py)
     streamingData.formats/adaptiveFormats
  5. Download with plain HTTP              (downloader/http.py)

Everything else (playlists, channels, HLS/DASH manifests, SABR, JS signature
decryption, n-challenge, PO tokens, subtitles, thumbnails, metadata, 100s of
other sites, ffmpeg merging...) is stripped away.

Usage:
  yt.py <url-or-id>            # download best progressive (muxed) MP4
  yt.py <url> -F               # list available formats
  yt.py <url> -f ITAG          # download a specific format by itag
  yt.py <url> -o out.mp4       # custom output filename
"""
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request

CLIENTS = [
    {
        'clientName': 'ANDROID',
        'clientVersion': '21.26.364',
        'androidSdkVersion': 30,
        'osName': 'Android',
        'osVersion': '11',
        'userAgent': 'com.google.android.youtube/21.26.364 (Linux; U; Android 11) gzip',
        'clientNameId': '3',
    },
    {
        'clientName': 'IOS',
        'clientVersion': '21.26.4',
        'deviceMake': 'Apple',
        'deviceModel': 'iPhone16,2',
        'userAgent': 'com.google.ios.youtube/21.26.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
        'clientNameId': '5',
    },
    {
        'clientName': 'VISIONOS',
        'clientVersion': '1.02',
        'deviceMake': 'Apple',
        'deviceModel': 'RealityDevice17,1',
        'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15',
        'osName': 'visionOS',
        'osVersion': '26.5.23O471',
        'clientNameId': '101',
    },
]


def http_get(url, headers=None):
    req = urllib.request.Request(url, headers=headers or {})
    return urllib.request.urlopen(req, timeout=30)


def extract_video_id(url):
    m = re.fullmatch(r'([\w-]{11})(?:\?\S*)?', url)
    if m:
        return m.group(1)
    m = re.search(
        r'(?:youtube\.com/(?:watch\?[^#]*v=|shorts/|embed/|live/)|youtu\.be/)'
        r'([\w-]{11})', url)
    if m:
        return m.group(1)
    sys.exit(f'error: could not extract a video ID from {url!r}')


def get_api_key(video_id):
    page = http_get(
        f'https://www.youtube.com/watch?v={video_id}',
        headers={'User-Agent': CLIENTS[-1]['userAgent']},
    ).read().decode('utf-8', 'replace')
    m = re.search(r'"INNERTUBE_API_KEY":"([^"]+)"', page)
    if not m:
        sys.exit('error: INNERTUBE_API_KEY not found on watch page')
    api_key = m.group(1)
    print(f'innertube api key: {api_key}')
    return api_key


def request_player(video_id, api_key):
    best, fmts, seen = None, [], set()
    last_error = 'no playable formats'
    for client in CLIENTS:
        print(f'\n--- querying innertube client: {client["clientName"]} ---')
        context = {k: v for k, v in client.items()
                   if k not in ('userAgent', 'clientNameId')}
        payload = {
            'context': {'client': context},
            'videoId': video_id,
            'playbackContext': {'contentPlaybackContext': {
                'html5Preference': 'HTML5_PREF_WANTS'}},
            'contentCheckOk': True,
            'racyCheckOk': True,
        }
        body = json.dumps(payload).encode()
        url = (f'https://www.youtube.com/youtubei/v1/player'
               f'?key={api_key}&prettyPrint=false')
        
        print(f'post url: {url}')
        print(f'post payload (summary): videoId={video_id}, client={client["clientName"]}')

        req = urllib.request.Request(url, data=body, headers={
            'Content-Type': 'application/json',
            'X-YouTube-Client-Name': client['clientNameId'],
            'X-YouTube-Client-Version': client['clientVersion'],
            'User-Agent': client['userAgent'],
        })
        try:
            data = json.load(urllib.request.urlopen(req, timeout=30))
        except (urllib.error.URLError, ValueError) as e:
            last_error = f'{client["clientName"]} request failed: {e}'
            print(f'request error: {last_error}')
            continue

        status = data.get('playabilityStatus', {})
        print(f'playability status: {status.get("status")}')
        
        # Display response preview
        keys = list(data.keys())
        print(f'json blob keys received: {keys}')
        if 'streamingData' in data:
            fmts_count = len(data['streamingData'].get('formats', []))
            adapt_count = len(data['streamingData'].get('adaptiveFormats', []))
            print(f'streamingData found: {fmts_count} muxed formats, {adapt_count} adaptive formats')

        if status.get('status') != 'OK':
            last_error = (f'{client["clientName"]}: '
                          f'{status.get("reason") or status.get("status")}')
            continue
        if best is None:
            best = data
        for f in parse_formats(data, client):
            key3 = (f['itag'], f['has_audio'], bool(f['height']))
            if key3 not in seen:
                seen.add(key3)
                fmts.append(f)
    if not fmts:
        sys.exit(f'error: video not playable — {last_error}')
    print('--------------------------------------------\n')
    return best, fmts


def parse_formats(player_response, client):
    fmts = []
    sd = player_response.get('streamingData', {})
    for f in sd.get('formats', []) + sd.get('adaptiveFormats', []):
        if 'url' not in f:
            continue
        mime = f.get('mimeType', '')
        fmts.append({
            'itag': f.get('itag'),
            'url': f['url'],
            'container': mime.split(';')[0].split('/')[-1] or 'mp4',
            'codecs': (mime.partition('codecs="')[2] or '').rstrip('"'),
            'width': f.get('width'), 'height': f.get('height'),
            'fps': f.get('fps'), 'bitrate': f.get('bitrate', 0),
            'has_audio': 'audioQuality' in f or f.get('audioChannels'),
            'size': int(f.get('contentLength') or 0),
            'ua': client['userAgent'],
            'label': f.get('qualityLabel')
                     or f.get('audioQuality', '').replace('AUDIO_QUALITY_', ''),
        })
    return fmts


def human(n):
    for unit in ('B', 'KiB', 'MiB', 'GiB'):
        if n < 1024 or unit == 'GiB':
            return f'{n:.1f} {unit}'
        n /= 1024


def describe(f):
    kind = 'muxed' if f['has_audio'] and f['height'] else \
           'video' if f['height'] else 'audio'
    return (f'{f["itag"]:>4}  {f["container"]:<5} {kind:<6} '
            f'{f["label"] or "?":<8} {human(f["size"]) if f["size"] else "?":>9}  '
            f'{f["codecs"]}')


def pick_format(fmts, itag=None):
    if itag is not None:
        for f in fmts:
            if f['itag'] == itag:
                return f
        sys.exit(f'error: itag {itag} not available; use -F to list formats')
    muxed = [f for f in fmts if f['has_audio'] and f['height']]
    pool = muxed or [f for f in fmts if f['height']] or fmts
    if not muxed:
        print('warning: no muxed format found; '
              'downloading the best video-only stream (no audio)', file=sys.stderr)
    return max(pool, key=lambda f: (f['height'] or 0, f['bitrate']))


def download(fmt, out_path):
    print(f'final download link: {fmt["url"]}')
    req = urllib.request.Request(fmt['url'], headers={'User-Agent': fmt['ua']})
    with urllib.request.urlopen(req, timeout=30) as r, open(out_path, 'wb') as fp:
        total = int(r.headers.get('Content-Length') or 0)
        got = 0
        while chunk := r.read(64 * 1024):
            fp.write(chunk)
            got += len(chunk)
            if total:
                pct = got / total * 100
                print(f'\r{pct:5.1f}% of {human(total)}', end='', flush=True)
        print(f'\rsaved -> {out_path} ({human(got)})' if total else
              f'saved -> {out_path} ({human(got)})')


def main():
    args = [a for a in sys.argv[1:] if not a.startswith('-')]
    opts = {sys.argv[i]: sys.argv[i + 1] for i in range(1, len(sys.argv) - 1)
            if sys.argv[i].startswith('-') and not sys.argv[i + 1].startswith('-')}
    if not args or '-h' in sys.argv or '--help' in sys.argv:
        sys.exit(__doc__)

    video_id = extract_video_id(args[0])
    print(f'video id: {video_id}')
    api_key = get_api_key(video_id)
    player, fmts = request_player(video_id, api_key)

    title = player.get('videoDetails', {}).get('title', video_id)
    print(f'title: {title}')
    print(f'{len(fmts)} downloadable format(s)')

    if '-F' in sys.argv:
        for f in sorted(fmts, key=lambda f: (-(f["height"] or 0), f["bitrate"])):
            print(f'{describe(f)}\n  -> {f["url"][:90]}...')
        return

    itag = int(opts['-f']) if '-f' in opts else None
    fmt = pick_format(fmts, itag)
    print('selected:', describe(fmt).strip())

    out = opts.get('-o') or re.sub(r'[\\/:*?"<>|]+', '_', title)
    if not out.lower().endswith('.' + fmt['container']):
        out += '.' + fmt['container']
    download(fmt, out)


if __name__ == '__main__':
    main()
Editor is loading...
Leave a Comment