Youtube Audio Mode

Listen to only the audio on YouTube without loading the video.

目前為 2022-07-26 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Youtube Audio Mode
// @description Listen to only the audio on YouTube without loading the video.
// @version     0.2.6
// @author      Burn
// @namespace   https://openuserjs.org/users/burn
// @copyright   2022, burn (https://openuserjs.org/users/burn)
// @include     https://www.youtube.com/*
// @match       https://www.youtube.com/*
// @license     MIT
// @run-at      document-end
// @grant       GM.setValue
// @grant       GM.getValue
// @noframes
// ==/UserScript==

/*

PLEASE NOTE

The original version of this userscript is written by Teaqu
and it's published here: https://github.com/teaqu/youtube-audio-mode

*/

(async function(open, originalFetch) {
    const DBG = false;
    let myLog = (msg) => {
        DBG && console.log(GM_info.script.name + " | " + msg);
    };

    window.addEventListener("yt-navigate-finish", audioMode);
    window.onYouTubeIframeAPIReady = await audioMode();
    if (document.getElementsByTagName("video")[0]) {
        myLog("adding listener to video tag: when file is ready to start playing (when it has buffered enough to begin)");
        document.getElementsByTagName("video")[0].addEventListener("canplay", handleTimeUpdate);
    }
// ontimeupdate
    async function audioMode() {
        if (location.pathname == "/watch") {
            let video = document.getElementsByTagName("video")[0];
            let audioMode = await GM.getValue("ytAudioMode");
            await addToMenu(audioMode);
            if (audioMode) {
                setPoster(video, ["maxres", "hq", "sd"]);
                watchFetchStream(video);
            } else {myLog("audiomode is disabled");}
        }
    }

    function handleTimeUpdate() {
        myLog("file is ready to start playing");
        myLog(JSON.stringify(arguments));
    }

    function watchFetchStream(video) {
        const constantMock = originalFetch;
        unsafeWindow.fetch = function() {
            return new Promise((resolve, reject) => {
                constantMock.apply(this, arguments)
                .then((response) => {
                    if(response.url.indexOf("mime=audio") > -1) { // && response.type != "cors"){
                        console.log(GM_info.script.name + " tipo risposta", response.type);
                        console.log(GM_info.script.name + " url risposta", response.url);
                        video.pause();
                        myLog("current time after pausing video: " + video.currentTime);
                        let currentTimeStamp = video.currentTime;
                        video.src = response.url.split("&range")[0];
                        myLog("other part splitted but not used: " + response.url.split("&range")[1]);
                        video.currentTime = currenetTimeStamp;
                        myLog("current time before resuming video playback: " + video.currentTime);
                        video.play();
                    }
                    resolve(response);
                })
                .catch((error) => {
                    reject(response);
                })
            });
        }
    }

    // Add audio mode to the settings menu
    async function addToMenu(audioMode) {
        let panel = document.getElementsByClassName("ytp-panel-menu")[0];
        if (!panel.innerHTML.includes("Audio Mode")) {
            panel.innerHTML += `
            <div class="ytp-menuitem"
                aria-checked="${audioMode}"
                id="audio-mode">
                <div class="ytp-menuitem-icon"></div>
                <div class="ytp-menuitem-label">Audio Mode</div>
                <div class="ytp-menuitem-content">
                    <div class="ytp-menuitem-toggle-checkbox">
                </div>
            </div>`;

            // Toggle audio mode on or off
            let audioToggle = document.getElementById("audio-mode");
            audioToggle.onclick = async function() {
                let audioMode = ! await GM.getValue("ytAudioMode");
                this.setAttribute("aria-checked", audioMode);
                GM.setValue("ytAudioMode", audioMode);
                location.reload();
            }
        }
    }

    // Set the video poster from thumbnails with the best avaliable format
    // https://developers.google.com/youtube/v3/docs/thumbnails
    async function setPoster(video, fmts) {
        let img = new Image();
        let videoId = location.search.match(/v=(.+?)(&|$)/)[1];
        img.src = `//i.ytimg.com/vi/${videoId}/${fmts.shift()}default.jpg`
        img.onload = function() {
            myLog("thumbnail loaded");
            // A height 90 is YouTube "not found" image.
            if (img.height <= 90) {
                myLog("thumbnail should be youtube not found image");
                setPoster(video, fmts);
            } else {
                myLog("thumbnail found, now applying css rule to display it");
                video.style.background = `url(${img.src}) no-repeat center`;
                video.style.backgroundSize = "contain";
            }
        };
    }
})(XMLHttpRequest.prototype.open, window.fetch || unsafeWindow.fetch);