#!/usr/bin/env python3
"""yt-check.py: compare a video file with what YouTube Help recommends for uploads.
Usage: python3 yt-check.py file.mp4
It reports OK / DIFF / INFO per item. DIFF means "differs from the recommendation", not "YouTube rejects it".
Recommendations read on 2026-10-30 from https://support.google.com/youtube/answer/1722171 (encoding),
https://support.google.com/youtube/answer/6375112 (aspect ratio, sizes), https://support.google.com/youtube/answer/71673 (limits)."""
import json, struct, subprocess, sys, os

SDR_MBPS = {  # height: (standard fps 24-30 low, high, high fps 48-60 low, high)
    2160: (35, 45, 53, 68), 1440: (16, 16, 24, 24), 1080: (8, 8, 12, 12),
    720: (5, 5, 7.5, 7.5), 480: (2.5, 2.5, 4, 4), 360: (1, 1, 1.5, 1.5)}
SIZES_16_9 = {(7680, 4320), (3840, 2160), (2560, 1440), (1920, 1080), (1280, 720), (854, 480), (640, 360), (426, 240)}
CONTAINERS = {b'moov', b'trak', b'edts', b'mdia', b'minf', b'stbl'}

def boxes(f, start, end, depth=0):
    """Yield (type, offset, size, depth) for boxes between start and end; recurse into a few container boxes."""
    pos = start
    while pos + 8 <= end:
        f.seek(pos)
        size, typ = struct.unpack('>I4s', f.read(8))
        hdr = 8
        if size == 1:
            size = struct.unpack('>Q', f.read(8))[0]; hdr = 16
        elif size == 0:
            size = end - pos
        if size < hdr:
            break
        yield typ, pos, size, depth
        if typ in CONTAINERS:
            yield from boxes(f, pos + hdr, pos + size, depth + 1)
        pos += size

def probe(path):
    out = subprocess.check_output(['ffprobe', '-v', 'error', '-show_format', '-show_streams', '-of', 'json', path])
    return json.loads(out)

def fps_of(v):
    n, d = v.get('avg_frame_rate', '0/1').split('/')
    return float(n) / float(d) if float(d) else 0.0

def main(path):
    res = []
    def add(tag, name, msg): res.append((tag, name, msg)); print(f'{tag:4} {name}: {msg}')
    p = probe(path)
    fmt = p['format']; vs = [s for s in p['streams'] if s['codec_type'] == 'video']; as_ = [s for s in p['streams'] if s['codec_type'] == 'audio']
    v = vs[0]
    names = fmt['format_name']
    add('OK' if 'mp4' in names else 'DIFF', 'container', f"{names} (recommended: MP4)")
    top = []; elst = 0
    with open(path, 'rb') as f:
        for t, off, size, depth in boxes(f, 0, os.path.getsize(path)):
            if depth == 0: top.append((t, off))
            if t == b'elst': elst += 1
    pos = {t: off for t, off in reversed(top)}
    if b'moov' in pos and b'mdat' in pos:
        add('OK' if pos[b'moov'] < pos[b'mdat'] else 'DIFF', 'fast start', 'moov atom before the media data' if pos[b'moov'] < pos[b'mdat'] else 'moov atom is after the media data (recommended: at the front)')
    else:
        add('INFO', 'fast start', 'no moov/mdat boxes found (not an MP4 file)')
    add('OK' if elst == 0 else 'DIFF', 'edit lists', f'{elst} edit list box(es) (recommended: none)')
    add('OK' if v['codec_name'] == 'h264' else 'DIFF', 'video codec', f"{v['codec_name']} (recommended: H.264)")
    if v['codec_name'] == 'h264':
        add('OK' if v.get('profile') == 'High' else 'DIFF', 'profile', f"{v.get('profile')} (recommended: High)")
    fo = v.get('field_order', 'unknown')
    add('OK' if fo in ('progressive', 'unknown') else 'DIFF', 'scan', f'{fo} (recommended: progressive)')
    if as_:
        a = as_[0]
        add('OK' if a['codec_name'] in ('aac', 'opus') else 'DIFF', 'audio codec', f"{a['codec_name']} (recommended: AAC-LC or Opus)")
        sr = int(a.get('sample_rate', 0))
        add('OK' if sr == 48000 else 'DIFF', 'audio sample rate', f'{sr} Hz (recommended: 48 kHz)')
    else:
        add('INFO', 'audio', 'no audio stream')
    w, h = v['width'], v['height']
    add('OK' if (w, h) in SIZES_16_9 else 'INFO', 'frame size', f'{w}x{h}' + ('' if (w, h) in SIZES_16_9 else ' is not one of the 16:9 sizes YouTube lists'))
    add('OK' if w * 9 == h * 16 else 'INFO', 'aspect ratio', f'{w}:{h} ' + ('is 16:9' if w * 9 == h * 16 else 'is not 16:9; the player adapts and may pad (YouTube Help)'))
    fps = fps_of(v); dur = float(fmt['duration']); size = int(fmt['size']); br = int(fmt['bit_rate']) / 1e6
    vbr = int(v.get('bit_rate', 0)) / 1e6 or br
    if h in SDR_MBPS:
        lo, hi, hlo, hhi = SDR_MBPS[h]
        tier = 'standard' if fps < 40 else 'high'
        rlo, rhi = (lo, hi) if tier == 'standard' else (hlo, hhi)
        tag = 'OK' if rlo * 0.9 <= vbr <= rhi * 1.1 else 'INFO'
        add(tag, 'video bit rate', f'{vbr:.2f} Mbps at {fps:.2f} fps; YouTube lists {rlo:g}' + (f'-{rhi:g}' if rhi != rlo else '') + f' Mbps for {h}p {tier} frame rate (SDR). Within 10% counts as OK; the list is a recommendation, not a limit')
    else:
        add('INFO', 'video bit rate', f'{vbr:.2f} Mbps; no recommendation row for height {h}')
    add('OK' if dur < 12 * 3600 else 'DIFF', 'length limit', f'{dur:.1f} s; limit 12 hours' + ('' if dur <= 900 else '; over 15 minutes needs a verified account'))
    add('OK' if size < 256 * 10**9 else 'DIFF', 'file size limit', f'{size} bytes; limit 256 GB')
    return res

if __name__ == '__main__':
    main(sys.argv[1])
