YouTube Quality Size (updated version of Abdelrahman Khalil's)

Show estimated file size next to each YouTube quality option

// ==UserScript==
// @name         YouTube Quality Size (updated version of Abdelrahman Khalil's)
// @namespace    ahmeed.yt.qs
// @version      0.1
// @description  Show estimated file size next to each YouTube quality option
// @author       ahmeed
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

;(() => {
    const DEFAULT_CODEC = 'vp9'
    const ALT_CODEC = 'avc1'
    const CACHE = {}

    const getFormats = async videoId => {
        if (CACHE[videoId]) return CACHE[videoId]

        const body = {
            videoId,
            context: {
                client: {
                    clientName: "WEB",
                    clientVersion: "2.20250920.01.00"
                }
            }
        }

        const res = await fetch(
            "https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "X-YouTube-Client-Name": "1",
                    "X-YouTube-Client-Version": "2.20250920.01.00"
                },
                body: JSON.stringify(body)
            }
        )

        const data = await res.json()
        CACHE[videoId] = data.streamingData?.adaptiveFormats || []
        return CACHE[videoId]
    }

    const getAudioContentLength = formats =>
        formats.find(
            f =>
                f.audioQuality === 'AUDIO_QUALITY_MEDIUM' &&
                f.mimeType.includes('opus')
        )?.contentLength || 0

    const mapFormats = (formats, qualityLabel, audioCL) =>
        formats
            .filter(f => f.qualityLabel === qualityLabel && f.contentLength)
            .map(vf => ({
                [matchCodec(vf.mimeType)]: toMBytes(vf.contentLength, audioCL)
            }))
            .reduce((r, c) => Object.assign(r, c), {})

    const matchCodec = mimeType => mimeType.replace(/(?!=").+="|\..+|"/g, '')
    const matchQLabel = qLabel => qLabel.replace(/\s.+/, '')
    const toMBytes = (vCL, aCL) => {
        const total = (parseInt(vCL) + parseInt(aCL)) / 1048576
        return total > 1024
            ? (total / 1024).toFixed(2) + " GB"
            : total.toFixed(1) + " MB"
    }
    const objectStringify = obj =>
        JSON.stringify(obj, null, ' ')
            .replace(/{|}|"|,/g, '')
            .trim()

    const createYQSNode = mappedFormats => {
        let YQSElement = document.createElement('yt-quality-size')
        YQSElement.style.marginLeft = '6px'
        YQSElement.style.fontSize = '0.85em'
        YQSElement.style.color = '#aaa'
        YQSElement.setAttribute('dir', 'ltr')
        YQSElement.setAttribute('title', objectStringify(mappedFormats))

        let textNode = document.createTextNode(
            mappedFormats[DEFAULT_CODEC] || mappedFormats[ALT_CODEC] || ''
        )
        YQSElement.appendChild(textNode)
        return YQSElement
    }

    const injectSizes = async () => {
        const menu = document.querySelector('.ytp-quality-menu')
        if (!menu || menu.querySelector('yt-quality-size')) return

        const videoId = new URLSearchParams(location.search).get('v')
        if (!videoId) return

        const formats = await getFormats(videoId)
        const audioCL = getAudioContentLength(formats)
        const qualitiesNode = menu.querySelectorAll('.ytp-menuitem-label')

        qualitiesNode.forEach((qualityNode, index) => {
            if (index === qualitiesNode.length - 1) return
            const qualityLabel = matchQLabel(qualityNode.textContent)
            const mappedFormats = mapFormats(formats, qualityLabel, audioCL)
            const YQSNode = createYQSNode(mappedFormats)
            qualityNode.appendChild(YQSNode)
        })
    }

    // checking constantly
    setInterval(injectSizes, 1500)
})()