GongYoo / YouBlock - youtube adblocker

// ==UserScript==
// @name         YouBlock - youtube adblocker
// @version      0.1.3
// @description  yt adblocker bypassing detections
// @match        https://www.youtube.com/*
// @match        https://www.youtube-nocookie.com/*
// @run-at       document-start
// @sandbox      raw
// @grant        none
// @license MIT
// @namespace https://greasyfork.org/users/1461063
// ==/UserScript==

(() => {
"use strict";

const Fetch = window.fetch;
const JSONParse = JSON.parse;
const JSONStringify = JSON.stringify;
const ResponseJSON = Response.prototype.json;
const XMLHttpRequestOpen = XMLHttpRequest.prototype.open;
const XMLHttpRequestSend = XMLHttpRequest.prototype.send;
const MapHas = Map.prototype.has;
const PromiseThen = Promise.prototype.then;
const FunctionToString = Function.prototype.toString;

const XMLHttpRequestUrls = new WeakMap();
const CallbackCache = new WeakMap();
const PendingNodes = new Set();

const AdSelectors = [
    "ytd-ad-slot-renderer",
    "ytd-display-ad-renderer",
    "ytd-promoted-video-renderer",
    "ytd-promoted-sparkles-web-renderer",
    "ytd-compact-promoted-video-renderer",
    "ytd-in-feed-ad-layout-renderer",
    "ytd-banner-promo-renderer",
    "ytd-action-companion-ad-renderer",
    "ytd-carousel-ad-renderer",
    "ytd-player-legacy-desktop-watch-ads-renderer",
    ".ytd-in-feed-ad-layout-renderer",
    ".ytd-display-ad-renderer"
].join(",");

const FeedWrappers = [
    "ytd-rich-item-renderer",
    "ytd-video-renderer",
    "ytd-grid-video-renderer",
    "ytd-compact-video-renderer",
    "ytd-reel-item-renderer"
].join(",");

const WarningSelectors = [
    "tp-yt-paper-dialog",
    "tp-yt-paper-toast",
    "ytd-enforcement-message-view-model",
    "ytd-modal-with-title-and-button-renderer",
    "yt-notification-action-renderer",
    "ytd-notification-action-renderer",
    "[role=\"alert\"]"
].join(",");

const RetryTypes = ["channel", "lactmilli"];
const ContractUserAgent = /(?:^|;\s*)(?:adunit|channel|lactmilli|instream|eafg)(?:;|$)/;

let RetryQueue = RetryTypes.slice();
let SnackbarDetected = false;
let SnackbarTime = 0;
let NormalUserAgent = null;
let PlayingVideoID = null;
let ReloadTime = 0;
let Frame = 0;

function HasAdFields(Data, Seen = new WeakSet()) {
    if (!Data || typeof Data !== "object" || Seen.has(Data)) return false;

    Seen.add(Data);

    for (const Name of Object.keys(Data)) {
        if (Name === "adPlacements" || Name === "adSlots") return true;
        if (HasAdFields(Data[Name], Seen)) return true;
    }

    return false;
}

function RewriteData(Data, Seen) {
    if (!Data || typeof Data !== "object") return Data;
    if (Seen.has(Data)) return Seen.get(Data);

    const Output = Array.isArray(Data) ? [] : {};

    Seen.set(Data, Output);

    for (const Name of Object.keys(Data)) {
        const NewName = Name === "adPlacements" || Name === "adSlots" ? "no_ads" : Name;
        Output[NewName] = RewriteData(Data[Name], Seen);
    }

    return Output;
}

function FixInitialResponse(Response) {
    if (!Response || typeof Response !== "object") return Response;
    if (!HasAdFields(Response)) return Response;

    return RewriteData(Response, new WeakMap());
}

function HookInitialResponse() {
    const Name = "ytInitialPlayerResponse";
    const Descriptor = Object.getOwnPropertyDescriptor(window, Name);

    if (Descriptor && !Descriptor.configurable) return;

    let SavedResponse;

    try {
        SavedResponse = FixInitialResponse(window[Name]);
    } catch {}

    try {
        Object.defineProperty(window, Name, {
            configurable: true,
            enumerable: Descriptor?.enumerable ?? true,

            get() {
                return SavedResponse;
            },

            set(Response) {
                SavedResponse = FixInitialResponse(Response);
            }
        });
    } catch {}
}

HookInitialResponse();

function GetURL(Input) {
    let Text;

    if (Input instanceof Request) {
        Text = Input.url;
    } else if (Input instanceof URL) {
        Text = Input.href;
    } else {
        Text = String(Input);
    }

    if (!Text.includes("/youtubei/v1/player") && !Text.includes("/youtubei/v1/get_watch")) return null;

    try {
        return new URL(Text, location.href);
    } catch {
        return null;
    }
}

function IsAdRequest(URL) {
    if (!URL) return false;
    return URL.pathname === "/youtubei/v1/player" || URL.pathname === "/youtubei/v1/get_watch";
}

function IsPlayerRequest(URL) {
    return typeof URL === "string" && URL.includes("/youtubei/v1/player");
}

function IsAdXMLRequest(URL) {
    return typeof URL === "string" && (URL.includes("/youtubei/v1/player") || URL.includes("/youtubei/v1/get_watch"));
}

function FixAdResponse(Text) {
    if (typeof Text !== "string") return Text;

    return Text
        .replace(/"adPlacements"/g, "\"no_ads\"")
        .replace(/"adSlots"/g, "\"no_ads\"");
}

function RebuildResponse(Response, Text) {
    const Headers = new window.Headers(Response.headers);

    Headers.delete("content-length");

    const Output = new window.Response(Text, {
        status: Response.status,
        statusText: Response.statusText,
        headers: Headers
    });

    try {
        Object.defineProperties(Output, {
            url: {
                value: Response.url,
                configurable: true
            },

            redirected: {
                value: Response.redirected,
                configurable: true
            },

            type: {
                value: Response.type,
                configurable: true
            }
        });
    } catch {}

    return Output;
}

window.fetch = new Proxy(Fetch, {
    apply: async (Target, ThisValue, Arguments) => {
        const URL = GetURL(Arguments[0]);

        if (!IsAdRequest(URL)) return Reflect.apply(Target, ThisValue, Arguments);

        const Response = await Reflect.apply(Target, ThisValue, Arguments);

        if (!Response.ok) return Response;

        let Text;

        try {
            Text = await Response.clone().text();
        } catch {
            return Response;
        }

        const Output = FixAdResponse(Text);

        if (Output === Text) return Response;
        return RebuildResponse(Response, Output);
    }
});

function IsShortsAd(Item) {
    return Item?.command?.reelWatchEndpoint?.adClientParams?.isAd === true;
}

function RemoveShortsAds(Data) {
    if (!Data || typeof Data !== "object") return Data;

    const Lists = [
        Data.entries,
        Data.reelWatchSequenceResponse?.entries
    ];

    for (const Items of Lists) {
        if (!Array.isArray(Items)) continue;

        for (let Index = Items.length - 1; Index >= 0; Index--) {
            if (IsShortsAd(Items[Index])) Items.splice(Index, 1);
        }
    }

    return Data;
}

JSON.parse = function(Text, Reviver) {
    const Data = JSONParse.call(this, Text, Reviver);

    if (typeof Text === "string" && Text.includes("reelWatchEndpoint") && Text.includes("isAd")) RemoveShortsAds(Data);
    return Data;
};

Response.prototype.json = async function() {
    const Data = await ResponseJSON.call(this);

    if (this.url.includes("/reel_watch_sequence")) RemoveShortsAds(Data);
    return Data;
};

function AddReloadMarker(Data, Seen = new WeakSet()) {
    if (!Data || typeof Data !== "object" || Seen.has(Data)) return;

    Seen.add(Data);

    for (const Name of Object.keys(Data)) {
        if (Name === "referer" && typeof Data[Name] === "string") {
            Data[Name] = Data[Name].replace(/(?:#reloadxhr)?$/, "#reloadxhr");
            continue;
        }

        AddReloadMarker(Data[Name], Seen);
    }
}

function FixPlayerRequestBody(Body) {
    if (typeof Body !== "string") return Body;

    let Request;

    try {
        Request = JSONParse(Body);
    } catch {
        return Body;
    }

    const Client = Request?.context?.client;
    const UserAgent = Client?.userAgent || "";

    if (Client?.clientName === "WEB" && UserAgent.includes("channel")) Client.clientScreen = "CHANNEL";

    if (UserAgent.includes("lactmilli")) {
        Request.params = "8AUB";

        const PlaybackContext = Request?.playbackContext?.contentPlaybackContext;
        if (PlaybackContext) PlaybackContext.lactMilliseconds = String(Date.now());
    }

    if (ContractUserAgent.test(UserAgent)) AddReloadMarker(Request);

    try {
        return JSONStringify(Request);
    } catch {
        return Body;
    }
}

XMLHttpRequest.prototype.open = function(Method, URL, ...Arguments) {
    const Text = String(URL);

    if (IsAdXMLRequest(Text)) {
        XMLHttpRequestUrls.set(this, Text);
    } else {
        XMLHttpRequestUrls.delete(this);
    }

    return XMLHttpRequestOpen.call(this, Method, URL, ...Arguments);
};

XMLHttpRequest.prototype.send = function(Body) {
    const URL = XMLHttpRequestUrls.get(this);

    if (IsPlayerRequest(URL)) Body = FixPlayerRequestBody(Body);
    return XMLHttpRequestSend.call(this, Body);
};

function HookXMLHttpRequest(Name) {
    const Descriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, Name);

    if (!Descriptor?.get || !Descriptor.configurable) return;

    Object.defineProperty(XMLHttpRequest.prototype, Name, {
        configurable: Descriptor.configurable,
        enumerable: Descriptor.enumerable,

        get() {
            const Output = Descriptor.get.call(this);

            if (!XMLHttpRequestUrls.has(this) || typeof Output !== "string") return Output;
            return FixAdResponse(Output);
        }
    });
}

try {
    HookXMLHttpRequest("responseText");
    HookXMLHttpRequest("response");
} catch {}

function RemoveUserAgentMarkers(UserAgent) {
    if (typeof UserAgent !== "string") return UserAgent;
    return UserAgent.replace(/;\s*(?:adunit|channel|lactmilli|instream|eafg)(?=[;)])/g, "").replace(/;\s*(?:adunit|channel|lactmilli|instream|eafg)$/g, "");
}

function GetYouTubeClient() {
    return window.ytcfg?.data_?.INNERTUBE_CONTEXT?.client || null;
}

function SaveUserAgent() {
    const Client = GetYouTubeClient();

    if (!Client?.userAgent) return false;
    if (NormalUserAgent === null) NormalUserAgent = RemoveUserAgentMarkers(Client.userAgent);
    return true;
}

function UseRetryMarker(Marker) {
    const Client = GetYouTubeClient();

    if (!Client?.userAgent) return false;

    SaveUserAgent();

    if (!NormalUserAgent) return false;

    if (!Marker) {
        Client.userAgent = NormalUserAgent;
        return true;
    }

    Client.userAgent = NormalUserAgent.replace(/(Mozilla\/5\.0 \([^)]+)/, "$1; " + Marker);
    return true;
}

function GetVideoID() {
    if (location.pathname !== "/watch") return null;
    return new URLSearchParams(location.search).get("v");
}

function ResetRetry(VideoID = GetVideoID()) {
    RetryQueue = RetryTypes.slice();
    SnackbarDetected = false;
    SnackbarTime = 0;
    PlayingVideoID = VideoID;
    ReloadTime = 0;

    UseRetryMarker("");
}

function GetPlayerInfo() {
    const Player = document.getElementById("movie_player");

    if (!Player) return null;

    try {
        return {
            Player,
            Response: Player.getPlayerResponse?.(),
            Progress: Player.getProgressState?.(),
            Stats: Player.getStatsForNerds?.(),
            State: Player.getPlayerStateObject?.()
        };
    } catch {
        return null;
    }
}

function IsDeadBuffer(PlayerInfo) {
    if (!PlayerInfo) return false;
    return PlayerInfo.State?.isBuffering === true && PlayerInfo.Stats?.buffer_health_seconds === "0.00 s" && PlayerInfo.Stats?.resolution === "0x0";
}

function SnackbarHit() {
    SnackbarTime = performance.now();

    if (SnackbarDetected) return;

    const PlayerInfo = GetPlayerInfo();

    if (!IsDeadBuffer(PlayerInfo) || RetryQueue.length === 0) return;

    const PlaybackStatsURL = PlayerInfo.Response?.playbackTracking?.videostatsPlaybackUrl?.baseUrl || "";

    if (PlaybackStatsURL.includes("reloadxhr")) RetryQueue.shift();
    SnackbarDetected = true;
}

Map.prototype.has = new Proxy(MapHas, {
    apply(Target, ThisValue, Arguments) {
        if (Arguments?.[0] === "onSnackbarMessage") SnackbarHit();
        return Reflect.apply(Target, ThisValue, Arguments);
    }
});

function IsBlockedCallback(Callback) {
    if (CallbackCache.has(Callback)) return CallbackCache.get(Callback);

    let Blocked = false;

    try {
        Blocked = FunctionToString.call(Callback).includes("onAbnormalityDetected");
    } catch {}

    CallbackCache.set(Callback, Blocked);
    return Blocked;
}

Promise.prototype.then = new Proxy(PromiseThen, {
    apply(Target, ThisValue, Arguments) {
        for (let Index = 0; Index < Arguments.length; Index++) {
            const Callback = Arguments[Index];

            if (typeof Callback !== "function") continue;
            if (!IsBlockedCallback(Callback)) continue;

            Arguments[Index] = function() {};
        }

        return Reflect.apply(Target, ThisValue, Arguments);
    }
});

function ReloadVideo(Player, VideoID, StartTime) {
    const CurrentTime = performance.now();

    if (CurrentTime - ReloadTime < 250) return;

    ReloadTime = CurrentTime;

    try {
        Player.loadVideoById(VideoID, StartTime);
    } catch {}
}

function CheckContract() {
    if (location.pathname !== "/watch") return;

    const PlayerInfo = GetPlayerInfo();

    if (!PlayerInfo) return;

    const Player = PlayerInfo.Player;
    const PlayerResponse = PlayerInfo.Response;
    const Progress = PlayerInfo.Progress;
    const Stats = PlayerInfo.Stats;
    const VideoID = PlayerResponse?.videoDetails?.videoId || GetVideoID();

    if (VideoID && VideoID !== PlayingVideoID) ResetRetry(VideoID);
    if (!PlayerResponse || !Progress) return;

    const HasVideo = Progress.duration > 0 && (Progress.loaded < Progress.duration || Progress.duration - Progress.current > 1) || PlayerResponse.videoDetails?.isLive === true;
    if (!HasVideo) return;

    if (Stats?.debug_info?.startsWith?.("SSAP, AD")) return;

    const StartTime = PlayerResponse?.playerConfig?.playbackStartConfig?.startSeconds ?? 0;

    if (PlayerResponse?.playabilityStatus?.status === "UNPLAYABLE") {
        const ErrorScreen = PlayerResponse?.playabilityStatus?.errorScreen;

        let ErrorText = "";

        try {
            ErrorText = JSONStringify(ErrorScreen?.playerErrorMessageRenderer?.subreason?.runs || ErrorScreen?.playerInterstitialRenderer?.content?.interstitialViewModel?.description?.commandRuns || []);
        } catch {}

        const ContractError = !ErrorScreen?.playerErrorMessageRenderer?.playerCaptchaViewModel && ErrorText.includes("WEB_PAGE_TYPE_UNKNOWN") && ErrorText.includes("https://support.google.com/youtube/answer/3037019");

        if (ContractError) {
            RetryQueue.shift();
            UseRetryMarker(RetryQueue[0] || "");
            SnackbarDetected = false;

            if (VideoID) ReloadVideo(Player, VideoID, StartTime);
            return;
        }
    }

    if (!RetryQueue.length) {
        SnackbarDetected = false;
        UseRetryMarker("");
        return;
    }

    if (!SnackbarDetected || !IsDeadBuffer(PlayerInfo)) return;

    UseRetryMarker(RetryQueue[0]);
    SnackbarDetected = false;

    if (VideoID) ReloadVideo(Player, VideoID, StartTime);
}

function RemoveAd(Element) {
    if (!(Element instanceof window.Element)) return;

    const Parent = Element.closest(FeedWrappers);

    if (Parent?.querySelector(AdSelectors)) {
        Parent.remove();
        return;
    }

    Element.remove();
}

function RemovePageAds(Root = document) {
    if (Root instanceof window.Element) {
        if (Root.matches(AdSelectors)) {
            RemoveAd(Root);
            return;
        }

        if (Root.matches(FeedWrappers) && Root.querySelector(AdSelectors)) {
            Root.remove();
            return;
        }
    }

    Root.querySelectorAll?.(AdSelectors).forEach(RemoveAd);

    Root.querySelectorAll?.(FeedWrappers).forEach(Item => {
        if (Item.querySelector(AdSelectors)) Item.remove();
    });
}

function RemoveWarnings(Root = document) {
    const Warnings = [];

    if (Root instanceof window.Element && Root.matches(WarningSelectors)) Warnings.push(Root);
    Root.querySelectorAll?.(WarningSelectors).forEach(Element => Warnings.push(Element));

    let RemovedWarning = false;

    for (const Element of Warnings) {
        const Text = (Element.innerText || Element.textContent || "").toLowerCase();
        const RecentSnackbar = performance.now() - SnackbarTime < 1500 && Element.matches("tp-yt-paper-toast, [role=\"alert\"]");

        if (RecentSnackbar || Text.includes("ad blocker") || Text.includes("adblock") || Text.includes("video player will be blocked") || Text.includes("allow youtube ads") || Text.includes("werbeblocker")) {
            Element.remove();
            RemovedWarning = true;
        }
    }

    if (!RemovedWarning) return;

    document.querySelectorAll("tp-yt-iron-overlay-backdrop").forEach(Element => Element.remove());

    if (document.body) document.body.style.overflow = "";
    document.documentElement.style.overflow = "";
}

function AddStyle() {
    if (!document.documentElement || document.getElementById("yt-adblock-style")) return;

    const Style = document.createElement("style");

    Style.id = "yt-adblock-style";
    Style.textContent = [
        "ytd-ad-slot-renderer",
        "ytd-display-ad-renderer",
        "ytd-promoted-video-renderer",
        "ytd-promoted-sparkles-web-renderer",
        "ytd-compact-promoted-video-renderer",
        "ytd-in-feed-ad-layout-renderer",
        "ytd-banner-promo-renderer",
        "ytd-action-companion-ad-renderer",
        "ytd-carousel-ad-renderer",
        "ytd-player-legacy-desktop-watch-ads-renderer",
        "ytd-rich-item-renderer:has(ytd-ad-slot-renderer)",
        "ytd-rich-item-renderer:has(ytd-display-ad-renderer)",
        "ytd-rich-item-renderer:has(ytd-promoted-video-renderer)",
        "ytd-rich-item-renderer:has(ytd-promoted-sparkles-web-renderer)",
        "ytd-rich-item-renderer:has(ytd-in-feed-ad-layout-renderer)",
        "ytd-video-renderer:has(ytd-ad-slot-renderer)",
        "ytd-video-renderer:has(ytd-promoted-video-renderer)",
        "ytd-grid-video-renderer:has(ytd-ad-slot-renderer)",
        "ytd-compact-video-renderer:has(ytd-ad-slot-renderer)",
        "ytd-reel-item-renderer:has(ytd-ad-slot-renderer)"
    ].join(",") + "{display:none!important;}";

    document.documentElement.appendChild(Style);
}

function FlushDOMWork() {
    Frame = 0;

    const Nodes = [...PendingNodes];

    PendingNodes.clear();

    for (const Node of Nodes) {
        if (!(Node instanceof window.Element)) continue;

        RemovePageAds(Node);
        RemoveWarnings(Node);
    }

    CheckContract();
}

function ScheduleDOMWork(Node) {
    if (Node instanceof window.Element) PendingNodes.add(Node);
    if (Frame) return;

    Frame = requestAnimationFrame(FlushDOMWork);
}

function Run(Root = document) {
    AddStyle();
    SaveUserAgent();
    RemovePageAds(Root);
    RemoveWarnings(Root);
    CheckContract();
}

const WatchTimer = setInterval(() => {
    SaveUserAgent();
    CheckContract();
}, 100);

const StartupTimer = setInterval(() => {
    if (!document.documentElement) return;

    clearInterval(StartupTimer);
    AddStyle();

    const Observer = new MutationObserver(Changes => {
        for (const Change of Changes) {
            for (const Node of Change.addedNodes) ScheduleDOMWork(Node);
        }
    });

    Observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

    Run();
}, 1);

document.addEventListener("yt-navigate-finish", () => {
    ResetRetry(GetVideoID());
    Run();
}, true);

document.addEventListener("yt-page-data-updated", () => Run(), true);

window.addEventListener("beforeunload", () => {
    clearInterval(WatchTimer);

    if (Frame) cancelAnimationFrame(Frame);
}, {once: true});
})();