#!/usr/bin/env python3
"""Check a file against Spotify for Creators' published upload specs (help pages read 2026-10-25). Needs ffprobe."""
import json, subprocess, sys, os
f = sys.argv[1]
j = json.loads(subprocess.check_output(["ffprobe","-v","error","-show_streams","-show_format","-of","json",f]))
fmt, st = j["format"], j["streams"]
v = [s for s in st if s["codec_type"]=="video" and s.get("disposition",{}).get("attached_pic",0)==0]
a = [s for s in st if s["codec_type"]=="audio"]
dur = float(fmt.get("duration",0)); size = int(fmt.get("size",0)); names = fmt["format_name"]
rows = []
def r(level, rule, got): rows.append((level, rule, got))
ext = os.path.splitext(f)[1].lower()
if not v:
    ok = ext in (".mp3",".m4a",".wav")
    r("PASS" if ok else "FAIL", "Audio file type is MP3, M4A or WAV", ext)
    r("PASS" if len(a)==1 and a[0]["channels"] in (1,2) else "FAIL", "Mono or stereo", f'{a[0]["channels"]} channel(s)')
    r("INFO", "Codec / sample rate / bitrate", f'{a[0]["codec_name"]}, {a[0]["sample_rate"]} Hz, {int(a[0].get("bit_rate",fmt.get("bit_rate",0)))//1000} kbps')
    r("INFO", "File size (no limit stated; large files can fail on a bad connection)", f"{size/1e6:.1f} MB")
else:
    vs, au = v[0], a[0] if a else None
    r("PASS" if ext in (".mp4",".mov") else "WARN", "Container MP4 (recommended) or MOV", ext)
    r("PASS" if len(v)==1 and len(a)==1 else "FAIL", "Exactly one video track and one audio track", f"{len(v)} video, {len(a)} audio")
    r("PASS" if vs["codec_name"]=="h264" else "WARN", "Video codec H.264 (H.265 also compatible)", vs["codec_name"]+" "+vs.get("profile",""))
    w,h = vs["width"], vs["height"]
    r("PASS" if min(w,h)>=1080 else "WARN", "Resolution 1080p or higher (recommended)", f"{w}x{h}")
    r("PASS" if abs(w/h-16/9)<0.01 else "WARN", "Aspect ratio 16:9 (others are letterboxed)", f"{w/h:.3f}")
    n,d = map(int, vs["avg_frame_rate"].split("/")); fps = n/d if d else 0
    r("PASS" if round(fps) in (24,25,30,50,60) else "WARN", "Frame rate 24, 25, 30, 50 or 60", f"{fps:.2f}")
    r("PASS" if vs.get("field_order","progressive") in ("progressive","unknown") else "FAIL", "Progressive scan", vs.get("field_order","progressive"))
    r("PASS" if vs.get("pix_fmt","") in ("yuv420p","yuv420p10le") else "WARN", "4:2:0 color sampling, 8 or 10 bit (recommended)", vs.get("pix_fmt",""))
    r("INFO" if "color_space" not in vs else ("PASS" if vs["color_space"]=="bt709" else "WARN"), "Color space Rec.709 (recommended; INFO = not tagged in the file)", vs.get("color_space","unset"))
    if au:
        r("PASS" if au["codec_name"]=="aac" and au.get("profile","LC")=="LC" else "WARN", "Audio AAC-LC (recommended)", au["codec_name"]+" "+au.get("profile",""))
        r("PASS" if int(au.get("bit_rate",0))>=128000 else "WARN", "Audio 128 kbps or higher (192+ in the upload article)", f'{int(au.get("bit_rate",0))//1000} kbps')
        r("PASS" if au["channels"]==2 else "WARN", "Stereo (mono is upmixed, 5.1 downmixed)", f'{au["channels"]} ch')
        dv, da = float(vs.get("duration",dur)), float(au.get("duration",dur))
        r("PASS" if abs(dv-da)<0.5 else "FAIL", "Audio and video tracks have the same duration", f"video {dv:.2f}s, audio {da:.2f}s")
    r("PASS" if dur<4*3600 else ("WARN" if dur<=12*3600 else "FAIL"), "Duration under 4 hours (recommended), 12 hours at most", f"{dur:.1f} s")
    r("PASS" if size<10e9 else ("WARN" if size<=60e9 else "FAIL"), "File size under 10 GB (recommended), 60 GB at most", f"{size/1e6:.1f} MB")
    if len(sys.argv)>2:
        k = subprocess.check_output(["ffprobe","-v","error","-select_streams","v:0","-skip_frame","nokey","-show_entries","frame=pts_time","-of","csv=p=0",f]).decode().split()
        t = [float(x.strip(",")) for x in k if x.strip(",")]
        gaps = [b-a_ for a_,b in zip(t,t[1:])]
        mg = max(gaps) if gaps else dur
        r("PASS" if mg<=1.05 else ("WARN" if mg<=60 else "FAIL"), "Keyframe about every 1 s (recommended), at least one per 60 s", f"{len(t)} keyframes, longest gap {mg:.2f} s")
for lvl,rule,got in rows: print(f"{lvl:5} {rule}  [{got}]")
print("RESULT", "FAIL" if any(x[0]=="FAIL" for x in rows) else "OK (WARN lines are recommendations, not errors)")
