YouTube Qualities Size

Shows file size for each quality in YouTube

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Qualities Size
// @namespace    ytqz.smz.k
// @version      1.2.6
// @description  Shows file size for each quality in YouTube
// @author       Abdelrahman Khalil
// @match        https://www.youtube.com/*
// @license      MIT
// ==/UserScript==

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

    // --------------------------------
    // API Stuff.
    // --------------------------------
    // NOT WORKING
    /*const fetchInfo = (videoId, detailpage = false) => {
        let url = `https://www.youtube.com/get_video_info?video_id=${videoId}&html5=1`

        // Retrive full info when video is copyrighted.
        if (detailpage) url += '&el=detailpage'

        return fetch(url)
    }

    // Youtube sends data as ugly ass url queries.
    // Parse it using URLSearchParams then get value of player_response which is stringified JSON.
    const parseInfo = uglyInfo =>
        JSON.parse(getQuery(uglyInfo, 'player_response')).streamingData
            .adaptiveFormats
    */
    const getFormats = async videoId => {
        /*let info = await fetchInfo(videoId).then(res => res.text())

        if (!info.includes('adaptiveFormats'))
            info = await fetchInfo(videoId, true).then(res => res.text())

        return parseInfo(info)*/

        //return ytplayer.config.args.raw_player_response.streamingData.adaptiveFormats
        console.clear()
        console.log(ytplayer)

        const body = {
            videoId,
            context: {
                client: {
                    clientName: 'WEB',
                    clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
                }
            }
        }

        return await fetch("https://www.youtube.com/youtubei/v1/player?key=" + ytcfg.data_.INNERTUBE_API_KEY, {method: "POST", body: JSON.stringify(body)}).then(res => res.json()).then(data => data.streamingData
            .adaptiveFormats)
    }

    // YouTube separates audio and video.
    const getAudioContentLength = formats =>
        formats.find(
            f =>
                f.audioQuality === 'AUDIO_QUALITY_MEDIUM' &&
                f.mimeType.includes('opus')
        ).contentLength || 0

    // Filter formats per quality.
    // Returns video content Length for all codecs summed by opus medium-quality audio content length.
    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), {})

    // --------------------------------
    // DOM Stuff.
    // --------------------------------
    const createYQSNode = mappedFormats => {
        let YQSElement = document.createElement('yt-quality-size')
        let isRTL = document.body.getAttribute('dir') === 'rtl'

        YQSElement.style.float = isRTL ? 'left' : 'right'
        YQSElement.style.textAlign = 'right'
        YQSElement.style.marginRight = '8px'
        YQSElement.setAttribute('dir', 'ltr')

        YQSElement.setAttribute('title', objectStringify(mappedFormats))

        let textNode = document.createTextNode(
            mappedFormats[DEFAULT_CODEC] || mappedFormats[ALT_CODEC] || ''
        )

        YQSElement.appendChild(textNode)

        return YQSElement
    }

    // Hook YQS element to each quality.
    const hookYQS = async addedNode => {
        let doesYQMExist =
                addedNode && addedNode.classList.contains('ytp-quality-menu'),
            doesYQSNotExist = !document.querySelector('yt-quality-size')

        if (doesYQMExist && doesYQSNotExist) {
            let YQM = addedNode,
                videoId = getQuery(location.search, 'v')

            if (!CACHE[videoId]) CACHE[videoId] = await getFormats(videoId)

            let formats = CACHE[videoId],
                qualitiesNode = YQM.querySelectorAll('span'),
                audioCL = getAudioContentLength(formats)

            qualitiesNode.forEach((qualityNode, index) => {
                if (index === qualitiesNode.length - 1) return

                let qualityLabel = matchQLabel(qualityNode.textContent),
                    mappedFormats = mapFormats(formats, qualityLabel, audioCL),
                    YQSNode = createYQSNode(mappedFormats)

                qualityNode.appendChild(YQSNode)
            })
        }
    }

    // Listen to page navigation and observe settings-menu if it's /watch.
    const onPageUpdate = () => {
        let SettingsMenuElement = document.querySelector('.ytp-settings-menu')

        if (SettingsMenuElement) {
            removeEventListener('yt-page-data-updated', onPageUpdate)
            new MutationObserver(([{ addedNodes }]) => {
                hookYQS(addedNodes[0])
            }).observe(SettingsMenuElement, { childList: true })
        }
    }

    addEventListener('yt-page-data-updated', onPageUpdate)

    // --------------------------------
    // Utils
    // --------------------------------
    const getQuery = (string, query) => new URLSearchParams(string).get(query)

    const matchCodec = mimeType => mimeType.replace(/(?!=").+="|\..+|"/g, '')
    const matchQLabel = qLabel => qLabel.replace(/\s.+/, '')

    const toMBytes = (vCL, aCL) => {
        let videoAudioMB = (parseInt(vCL) + parseInt(aCL)) / 1048576
        return (Math.round(videoAudioMB) || videoAudioMB.toFixed(2)) + ' MB'
    }

    const objectStringify = obj =>
        JSON.stringify(obj, null, '‎')
            .replace(/{|}|"|,/g, '')
            .trim()
})()