Greasy Fork is available in English.

Netflix Plus

Enable best audio and video and more features on Netflix

Installera detta skript?
Författaren's rekommenderade skript

Du kanske också gillar Netflix UHD.

Installera detta skript
// ==UserScript==
// @name                 Netflix Plus
// @name:ja              Netflix Plus
// @name:zh-CN           Netflix Plus
// @name:zh-TW           Netflix Plus
// @namespace            http://tampermonkey.net/
// @version              2.15
// @description          Enable best audio and video and more features on Netflix
// @description:ja       Netflixで最高の音質と画質、そしてもっと多くの機能を体験しましょう
// @description:zh-CN    在 Netflix 上开启最佳音视频质量和更多功能
// @description:zh-TW    在 Netflix 上啓用最佳影音品質和更多功能
// @author               TGSAN
// @match                https://www.netflix.com/*
// @icon                 https://www.google.com/s2/favicons?sz=64&domain=netflix.com
// @run-at               document-start
// @sandbox              raw
// @grant                unsafeWindow
// @grant                GM_setValue
// @grant                GM_getValue
// @grant                GM_registerMenuCommand
// @grant                GM_unregisterMenuCommand
// ==/UserScript==

(async () => {
    "use strict";

    let windowCtx = self.window;
    if (self.unsafeWindow) {
        console.log("[Netflix Plus] use unsafeWindow mode");
        windowCtx = self.unsafeWindow;
    } else {
        console.log("[Netflix Plus] use window mode (your userscript extensions not support unsafeWindow)");
    }

    // Disable Cache
    {
        const meta = document.createElement('meta');
        meta.httpEquiv = "Cache-Control";
        meta.content = "no-cache";
        windowCtx.document.head.appendChild(meta);
    }
    {
        const meta = document.createElement('meta');
        meta.httpEquiv = "Pragma";
        meta.content = "no-cache";
        windowCtx.document.head.appendChild(meta);
    }
    {
        const meta = document.createElement('meta');
        meta.httpEquiv = "Expires";
        meta.content = "-1";
        windowCtx.document.head.appendChild(meta);
    }

    function createToast() {
        let toast = document.createElement("div");
        toast.style.position = "fixed";
        toast.style.top = "20px";
        toast.style.left = "50%";
        toast.style.transform = "translateX(-50%)";
        toast.style.padding = "10px 20px";
        toast.style.backgroundColor = "rgba(250, 250, 250, 1.0)";
        toast.style.color = "rgba(32, 32, 32, 1.0)";
        toast.style.fontSize = "12px";
        toast.style.textAlign = "center";
        toast.style.fontWeight = "600";
        toast.style.zIndex = "9999";
        toast.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.25)";
        toast.style.borderRadius = "30px";
        toast.style.opacity = "0.0";
        toast.style.transition = "opacity 0.5s";
        document.body.appendChild(toast);
        return toast;
    }

    function showToast(message, time = 1500) {
        let toast = createToast();
        toast.innerText = message;
        setTimeout(function () {
            toast.style.opacity = "1.0";
            setTimeout(function () {
                toast.style.opacity = "0.0";
                setTimeout(function () {
                    document.body.removeChild(toast);
                }, 500);
            }, time);
        }, 1);
    }

    let playercoreDom = undefined;
    let startCaptureFunctionExec = true;
    windowCtx.Function.prototype.callNetflixPlusOriginal = windowCtx.Function.prototype.call;
    windowCtx.Function.prototype.call = function (...args) {
        if (startCaptureFunctionExec) {
            let funcStr = this.toString();
            let funcLen = funcStr.length;
            if (funcLen > 1000000) {
                // find original playercore not netflix plus playercore
                if (funcStr.indexOf("h264mpl") > -1 && funcStr.indexOf("videoElementNetflixPlus") < 0) {
                    console.log("PlayerCore found len: " + funcStr.length);
                    loadCustomPlayerCore();
                    return undefined;
                }
            }
        }
        return this.callNetflixPlusOriginal(...args);
    }

    if (windowCtx.netflix !== undefined && windowCtx.netflix.player !== undefined) {
        showToast("Netflix Plus is executing too late and is being forced to run, which may cause issues.\n\nRefresh the page while holding down the \"Shift\" key to resolve the issue.", 10000);
        console.warn("The user script is executing too late and is being forced to run, which may cause issues.");
        loadCustomPlayerCore();
    }

    function loadCustomPlayerCore() {
        startCaptureFunctionExec = false;
        if (playercoreDom == undefined) {
            for (let element of windowCtx.document.getElementsByTagName("script")) {
                if (element.src && element.src.indexOf("cadmium-playercore") > -1) {
                    playercoreDom = element;
                    break;
                }
            }
        }
        let playercore = document.createElement('script');
        playercore.src = "https://www.cloudmoe.com/static/userscript/netflix-plus/cadmium-playercore.js";
        // playercore.crossOrigin = playercoreDom.crossOrigin;
        playercore.async = playercoreDom.async;
        playercore.id = playercoreDom.id;
        playercoreDom.replaceWith(playercore);
    }

    // Register Netflix Plus Functions

    windowCtx._videoElementNetflixPlus;
    Object.defineProperty(windowCtx, "videoElementNetflixPlus", {
        get: function () { return windowCtx._videoElementNetflixPlus; },
        set: function (element) {
            let backup = windowCtx._videoElementNetflixPlus;
            windowCtx._videoElementNetflixPlus = element;
            element.addEventListener('playing', function () {
                if (backup === element) {
                    return;
                }

                if (!windowCtx.globalOptions.setMaxBitrate) {
                    return;
                }

                let getElementByXPath = function (xpath) {
                    return document.evaluate(
                        xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
                    ).singleNodeValue;
                };

                let selectFun = function () {
                    windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
                        keyCode: 83, // S (Old)
                        ctrlKey: true,
                        altKey: true,
                        shiftKey: true,
                    }));

                    windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
                        keyCode: 66, // B
                        ctrlKey: true,
                        altKey: true,
                        shiftKey: true,
                    }));

                    const VIDEO_SELECT = getElementByXPath("//div[text()='Video Bitrate / VMAF']");
                    const AUDIO_SELECT = getElementByXPath("//div[text()='Audio Bitrate']");
                    const BUTTON = getElementByXPath("//button[text()='Override']");
                    if (VIDEO_SELECT && AUDIO_SELECT && BUTTON) {
                        [VIDEO_SELECT, AUDIO_SELECT].forEach(function (el) {
                            let parent = el.parentElement;

                            let selects = parent.querySelectorAll('select');

                            selects.forEach(function (select) {
                                select.removeAttribute("disabled");
                            });

                            let options = parent.querySelectorAll('select > option');

                            for (var i = 0; i < options.length - 1; i++) {
                                options[i].removeAttribute('selected');
                            }

                            options[options.length - 1].setAttribute('selected', 'selected');
                        });

                        setTimeout(function() { BUTTON.click(); }, 100);

                        backup = element;
                    } else {
                        setTimeout(selectFun, 100);
                    }
                }
                selectFun();
            });
        }
    });

    windowCtx.modifyFilterNetflixPlus = function (ModList, ModConfig, DRMType) {
        let DrmVersion = "playready" === DRMType ? 30 : 0;
        if (windowCtx.globalOptions.useprk) {
            ModList.push("h264mpl30-dash-playready-prk-qc");
            ModList.push("h264mpl31-dash-playready-prk-qc");
            ModList.push("h264mpl40-dash-playready-prk-qc");
        }
        if (DrmVersion == 30) {
            if (windowCtx.globalOptions.useddplus) {
                ModList.push("ddplus-2.0-dash");
                ModList.push("ddplus-5.1-dash");
                ModList.push("ddplus-5.1hq-dash");
                ModList.push("ddplus-atmos-dash");
                // ModList = ModList.filter(item => { if (!new RegExp(/heaac/g).test(JSON.stringify(item))) return item; });
            }
            if (windowCtx.globalOptions.usehevc) {
                ModList = ModList.filter(item => { if (!new RegExp(/main10-L5/g).test(JSON.stringify(item))) return item; });
            }
            if (windowCtx.globalOptions.usef12k) {
                ModList = ModList.filter(item => { if (!new RegExp(/hevc-main10-L.*-dash-cenc-prk-do/g).test(JSON.stringify(item))) return item; });
            }
            if (windowCtx.globalOptions.usef4k) {
                ModList.push("hevc-main10-L30-dash-cenc");
                ModList.push("hevc-main10-L31-dash-cenc");
                ModList.push("hevc-main10-L40-dash-cenc");
                ModList.push("hevc-main10-L41-dash-cenc");
            }
        } else {
            if (windowCtx.globalOptions.useFHD) {
                ModList.push("playready-h264mpl40-dash");
                ModList.push("playready-h264hpl40-dash");
                ModList.push("vp9-profile0-L40-dash-cenc");
                ModList.push("av1-main-L50-dash-cbcs-prk");
                ModList.push("av1-main-L51-dash-cbcs-prk");
            }
            if (windowCtx.globalOptions.useHA) {
                ModList.push("heaac-5.1-dash");
            }
            if (!windowCtx.globalOptions.usedef) {
                if (windowCtx.globalOptions.useav1) {
                    ModList.push("av1-main-L20-dash-cbcs-prk");
                    ModList.push("av1-main-L21-dash-cbcs-prk");
                    ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
                }
                if (windowCtx.globalOptions.usevp9) {
                    ModList.push("vp9-profile0-L21-dash-cenc");
                    ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
                }
                if (windowCtx.globalOptions.useAVCH) {
                    ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/h264mp/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
                }
                if (windowCtx.globalOptions.useAVC) {
                    ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/h264hp/g).test(JSON.stringify(item))) return item; });
                    ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
                }
            }
        }
        if (windowCtx.globalOptions.useallSub) {
            ModConfig.showAllSubDubTracks = 1
        }
        if (windowCtx.globalOptions.closeimsc) {
            ModList = ModList.filter(item => { if (!new RegExp(/imsc1.1/g).test(JSON.stringify(item))) return item; });
        }
        return [ModList, ModConfig, DRMType];
    };

    // Main Logic

    const Event = class {
        constructor(script, target) {
            this.script = script;
            this.target = target;

            this._cancel = false;
            this._replace = null;
            this._stop = false;
        }

        preventDefault() {
            this._cancel = true;
        }
        stopPropagation() {
            this._stop = true;
        }
        replacePayload(payload) {
            this._replace = payload;
        }
    };

    let callbacks = [];
    windowCtx.addBeforeScriptExecuteListener = (f) => {
        if (typeof f !== "function") {
            throw new Error("Event handler must be a function.");
        }
        callbacks.push(f);
    };
    windowCtx.removeBeforeScriptExecuteListener = (f) => {
        let i = callbacks.length;
        while (i--) {
            if (callbacks[i] === f) {
                callbacks.splice(i, 1);
            }
        }
    };

    const dispatch = (script, target) => {
        if (script.tagName !== "SCRIPT") {
            return;
        }

        const e = new Event(script, target);

        if (typeof windowCtx.onbeforescriptexecute === "function") {
            try {
                windowCtx.onbeforescriptexecute(e);
            } catch (err) {
                console.error(err);
            }
        }

        for (const func of callbacks) {
            if (e._stop) {
                break;
            }
            try {
                func(e);
            } catch (err) {
                console.error(err);
            }
        }

        if (e._cancel) {
            script.textContent = "";
            script.remove();
        } else if (typeof e._replace === "string") {
            script.textContent = e._replace;
        }
    };
    const observer = new MutationObserver((mutations) => {
        for (const m of mutations) {
            for (const n of m.addedNodes) {
                dispatch(n, m.target);
            }
        }
    });
    observer.observe(document, {
        childList: true,
        subtree: true,
    });

    const menuItems = [
        ["setMaxBitrate", "Automatically select best bitrate available"],
        ["useallSub", "Show all audio-tracks and subs"],
        ["closeimsc", "Use SUP subtitle replace IMSC subtitle"],
        ["useDDPandHA", "Enable Dolby and HE-AAC 5.1 Audio"],
        ["useFHDAndAVCH", "Focus 1080P and High-AVC"],
    ];
    let menuCommandList = [];

    windowCtx.globalOptions = {
        useFHDAndAVCH: false,
        useDDPandHA: true,
        setMaxBitrate: true,
        useallSub: true,
        get ["useddplus"]() {
            return windowCtx.globalOptions.useDDPandHA;
        },
        useAVC: false,
        get ["useFHD"]() {
            return windowCtx.globalOptions.useFHDAndAVCH;
        },
        usedef: false,
        get ["useHA"]() {
            return windowCtx.globalOptions.useDDPandHA;
        },
        get ["useAVCH"]() {
            return windowCtx.globalOptions.useFHDAndAVCH;
        },
        usevp9: false,
        useav1: false,
        useprk: true,
        usehevc: false,
        usef4k: true,
        usef12k: false,
        closeimsc: true
    };

    windowCtx.globalOptions.useFHDAndAVCH = !await checkAdvancedDrm();

    windowCtx.onbeforescriptexecute = function (e) {
        let scripts = document.getElementsByTagName("script");
        if (scripts.length === 0) return;
        for (let i = 0; scripts.length > i; i++) {
            let dom = scripts[i];
            if (dom.src.includes("cadmium-playercore")) {
                // firefox cannot reload src after change src url
                // dom.src = "https://static.cloudmoe.com/res/userscript/netflix-plus/cadmium-playercore.js";
                playercoreDom = dom;
                console.warn("parsing playercore dom");
                windowCtx.onbeforescriptexecute = null;
                break;
            }
        }
    };

    async function checkAdvancedDrm() {
        let supported = false;
        if (windowCtx.MSMediaKeys) {
            supported = true;
        }
        if (windowCtx.WebKitMediaKeys) {
            supported = true;
        }
        // Check L1
        let options = [
            {
                "videoCapabilities": [
                    {
                        "contentType": "video/mp4;codecs=avc1.42E01E",
                        "robustness": "HW_SECURE_ALL"
                    }
                ]
            }
        ];

        try {
            await navigator.requestMediaKeySystemAccess("com.widevine.alpha.experiment", options);
            supported = true;
        } catch { }
        console.log("Supported advanced DRM: " + supported);
        return supported;
    }

    async function checkSelected(type) {
        let selected = await GM_getValue("NETFLIX_PLUS_" + type);
        if (typeof selected == "boolean") {
            return selected;
        } else {
            return windowCtx.globalOptions[type];
        }
    }

    async function registerSelectableVideoProcessingMenuCommand(name, type) {
        let selected = await checkSelected(type);
        windowCtx.globalOptions[type] = selected;
        return await GM_registerMenuCommand((await checkSelected(type) ? "✅" : "🔲") + " " + name, async function () {
            await GM_setValue("NETFLIX_PLUS_" + type, !selected);
            windowCtx.globalOptions[type] = !selected;
            updateMenuCommand();
        });
    }

    async function updateMenuCommand() {
        for (let command of menuCommandList) {
            await GM_unregisterMenuCommand(command);
        }
        menuCommandList = [];
        for (let menuItem of menuItems) {
            menuCommandList.push(await registerSelectableVideoProcessingMenuCommand(menuItem[1], menuItem[0]));
        }
    }

    updateMenuCommand();
})();