Watch9 Reconstruct

Restores the old watch layout from before 2019

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Watch9 Reconstruct
// @version      2.5.0
// @description  Restores the old watch layout from before 2019
// @author       Aubrey P.
// @icon         https://www.youtube.com/favicon.ico
// @namespace    aubymori
// @license      Unlicense
// @match        www.youtube.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

const w9rOptions = {
    oldAutoplay: true,        // Classic autoplay renderer with "Up next" text
    removeBloatButtons: true  // Removes "Clip", "Thanks", "Download", etc.
}

/**
 * Localization strings.
 *
 * See LOCALIZATION.md in the GitHub repo.
 */
 const w9ri18n = {
    en: {
        subSuffixMatch: /( subscribers)|( subscriber)/,
        nonPublishMatch: /(Premier)|(Stream)|(Start)/,
        publishedOn: "Published on %s",
        uploadedOn: "Uploaded on %s",
        upNext: "Up next",
        autoplay: "Autoplay",
        autoplayTip: "When autoplay is enabled, a suggested video will automatically play next."
    },
    ja: {
        subSuffixMatch: /(チャンネル登録者数 )|(人)/g,
        nonPublishMatch: /(公開済)|(開始済)/g,
        publishedOn: "%s に公開",
        uploadedOn: "%s にアップロード",
        upNext: "自動再生",
        autoplay: "次の動画",
        autoplayTip: "自動再生を有効にすると、関連動画が自動的に再生されます。"
    },
    pl: {
        subSuffixMatch: /( subskrybentów)|( subskrybent)/,
        nonPublishMatch: /(Data premiery: )|(adawane na żywo )|(Transmisja zaczęła się )/,
        publishedOn: "Przesłany %s",
        uploadedOn: "Przesłany %s",
        upNext: "Następny",
        autoplay: "Autoodtwarzanie",
        autoplayTip: "Jeśli masz włączone autoodtwarzanie, jako następny włączy się automatycznie proponowany film."
    },
    fil: {
        subSuffixMatch: /(na)|( subscribers)|( subscriber)|(\s)/g,
        nonPublishMatch: /(simula)/,
        publishedOn: "Na-publish noong %s",
        uploadedOn: "Na-upload noong %s",
        upNext: "Susunod",
        autoplay: "I-autoplay",
        autoplayTip: "Kapag naka-enable ang autoplay, awtomatikong susunod na magpe-play ang isang iminumungkahing video."
    },
    fr: {
        subSuffixMatch: /( abonnés)|( abonné)|( d’abonnés)|( d’abonné)/g,
        nonPublishMatch: /(Diffus)|(Sortie)/g,
        publishedOn: "Publiée le %s",
        uploadedOn: "Mise en ligne le %s",
        upNext: "À suivre",
        autoplay: "Lecture automatique",
        autoplayTip: "Lorsque cette fonctionnalité est activée, une vidéo issue des suggestions est automatiquement lancée à la suite de la lecture en cours."
    },
    es: {
        subSuffixMatch: /( de suscriptores)|( suscriptor)/g,
        nonPublishMatch: /(directo)|(Fecha)/g,
        publishedOn: "Publicado el %s",
        uploadedOn: "Subido el %s",
        upNext: "A continuación",
        autoplay: "Reproducción automática",
        autoplayTip: "Si la reproducción automática está habilitada, se reproducirá automáticamente un vídeo a continuación."
    },
    pt: {
        subSuffixMatch: /( de subscritores)|( subscritor)/g,
        nonPublishMatch: /(Stream)|(Estreou)/g,
        publishedOn: "Publicado a %s",
        uploadedOn: "Carregado a %s",
        upNext: "Próximo",
        autoplay: "Reprodução automática",
        autoplayTip: "Quando a reprodução automática é ativada, um vídeo sugerido será executado automaticamente em seguida."
    },
    ru: {
        subSuffixMatch: /( подписчиков)|( подписчик)/g,
        nonPublishMatch: /(Сейчас смотрят:)|(Прямой эфир состоялся)|(Дата премьеры:)/g,
        publishedOn: "Дата публикации: %s",
        uploadedOn: "Дата публикации: %s",
        upNext: "Следующее видео",
        autoplay: "Автовоспроизведение",
        autoplayTip: "Если функция включена, то следующий ролик начнет воспроизводиться автоматически."
    }
};

/**
 * Wait for a selector to exist
 *
 * @param {string}       selector  CSS Selector
 * @param {HTMLElement}  base      Element to search inside
 * @returns {Node}
 */
async function waitForElm(selector, base = document) {
    if (!selector) return null;
    if (!base.querySelector) return null;
    while (base.querySelector(selector) == null) {
        await new Promise(r => requestAnimationFrame(r));
    };
    return base.querySelector(selector);
};

/**
 * Get a string from the localization strings.
 *
 * @param {string} string  Name of string to get
 * @param {string} hl      Language to use.
 * @returns {string}
 */
function getString(string, hl = "en") {
    if (!string) return "ERROR";
    if (w9ri18n[hl]) {
        if (w9ri18n[hl][string]) {
            return w9ri18n[hl][string];
        } else if (w9ri18n.en[string]) {
            return w9ri18n.en[string];
        } else {
            return "ERROR";
        }
    } else {
        if (w9ri18n.en[string]) return w9ri18n.en[string];
        return "ERROR";
    }
}

/**
 * Format upload date string to include "Published on" or "Uploaded on" if applicable.
 *
 * @param {string}  dateStr  dateText from InnerTube ("Sep 13, 2022", "Premiered 2 hours ago", etc.)
 * @param {boolean} isPublic Is the video public?
 * @param {string}  hl       Language to use.
 * @returns {string}
 */
function formatUploadDate(dateStr, isPublic, hl = "en") {
    var nonPublishMatch = getString("nonPublishMatch", hl);
    var string = isPublic ? getString("publishedOn", hl) : getString("uploadedOn", hl);
    if (nonPublishMatch.test(dateStr)) {
        return dateStr;
    } else {
        return string.replace("%s", dateStr);
    }
}

/**
 * Format subscriber count string to only include count.
 *
 * @param {string} count  Subscriber count string from InnerTube ("374K subscribers", "No subscribers", etc.)
 * @param {string} hl     Language to use.
 * @returns {string}
 */
function formatSubCount(count, hl = "en") {
    if (count == null) return "";
    var tmp = count.replace(getString("subSuffixMatch", hl), "");
    return tmp;
}

/**
 * Parse document.cookie
 *
 * @returns {object}
 */
function parseCookies() {
    var c = document.cookie.split(";"), o = {};
    for (var i = 0, j = c.length; i < j; i++) {
        var s = c[i].split("=");
        var n = s[0].replace(" ", "");
        s.splice(0, 1);
        s = s.join("=");
        o[n] = s;
    }
    return o;
}

/**
 * Parse YouTube's PREF cookie.
 *
 * @param {string} pref  PREF cookie content
 * @returns {object}
 */
function parsePref(pref) {
    var a = pref.split("&"), o = {};
    for (var i = 0, j = a.length; i < j; i++) {
        var b = a[i].split("=");
        o[b[0]] = b[1];
    }
    return o;
}

/**
 * Is autoplay enabled?
 *
 * @returns {boolean}
 */
function autoplayState() {
    var cookies = parseCookies();
    if (cookies.PREF) {
        var pref = parsePref(cookies.PREF);
        if (pref.f5) {
            return !(pref.f5 & 8192)
        } else {
            return true; // default
        }
    } else {
        return true;
    }
}

/**
 * Toggle autoplay.
 *
 * @returns {void}
 */
function clickAutoplay() {
    var player = document.querySelector("#movie_player");
    var autoplay;
    if (autoplay = player.querySelector(".ytp-autonav-toggle-button-container")) {
        autoplay.parentNode.click();
    } else {
        var settings = player.querySelector('.ytp-settings-button');
        settings.click();settings.click();
        var item = player.querySelector('.ytp-menuitem[role="menuitemcheckbox"]');
        item.click();
    }
}

/**
 * Should the Autoplay renderer be inserted?
 * (Basically, if there's a playlist active)
 *
 * @returns {boolean}
 */
function shouldHaveAutoplay() {
    var playlist;
    if (playlist = document.querySelector("#playlist.ytd-watch-flexy")) {
        if (playlist.hidden && playlist.hidden == true) {
            return true;
        } else {
            return false;
        }
    } else {
        return true;
    }
}

/**
 * Is a value in an array?
 * 
 * @param {*}     needle    Value to search
 * @param {Array} haystack  Array to search
 * @returns {boolean}
 */
function inArray(needle, haystack) {
    for (var i = 0; i < haystack.length; i++) {
        if (needle == haystack[i]) return true;
    }
    return false;
}

/**
 * Remove bloaty action buttons.
 *
 * @returns {void}
 */
function removeBloatButtons() {
    var primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
    var actionBtns = primaryInfo.data.videoActions.menuRenderer.topLevelButtons;

    // Remove the action buttons accordingly.
    for (var i = 0; i < actionBtns.length; i++) {
        if (actionBtns[i].downloadButtonRenderer) {
            actionBtns.splice(i, 1);
            i--;
        } else if (actionBtns[i].buttonRenderer) {
            if (inArray(actionBtns[i].buttonRenderer.icon.iconType, ["MONEY_HEART", "CONTENT_CUT"])) {
                actionBtns.splice(i, 1);
                i--;
            }
        }
    }

    // Refresh the primary info's data.
    var tmp = primaryInfo.data;
    primaryInfo.data = {};
    primaryInfo.data = tmp;
}

/**
 * Is the current video public? Or is it unlisted/private?
 *
 * @returns {boolean}
 */
function isVideoPublic() {
    const primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
    if (primaryInfo.data.badges == null) return true;
    const badges = primaryInfo.data.badges;

    for (var i = 0; i < badges.length; i++) {
        var iconType = badges[i].metadataBadgeRenderer.icon.iconType;
        if (iconType == "PRIVACY_UNLISTED" || iconType == "PRIVACY_PRIVATE") {
            return false;
        }
    }
    return true;
}

/**
 * Get sidebar data.
 *
 * @returns {object}
 */
async function getSidebarData() {
    const secondaryResults = document.querySelector("ytd-watch-next-secondary-results-renderer");
    const resultData = secondaryResults.data.results;
    var response = {};

    if (yt.config_.LOGGED_IN == false) {
        response.element = await waitForElm("#items.ytd-watch-next-secondary-results-renderer");
        response.data = resultData;
        response.class = "ytd-watch-next-secondary-results-renderer";
        return response;
    } else {
        var tmp;
        if (tmp = resultData[0].relatedChipCloudRenderer) {
            response.element = await waitForElm("#contents.ytd-item-section-renderer", secondaryResults);
            response.data = resultData[1].itemSectionRenderer.contents;
            response.class = "ytd-item-section-renderer";
            return response;
        } else {
            response.element = await waitForElm("#items.ytd-watch-next-secondary-results-renderer");
            response.data = resultData;
            response.class = "ytd-watch-next-secondary-results-renderer";
            return response;
        }
    }
}

/**
 * Build the classic compact autoplay renderer.
 *
 * @returns {void}
 */
async function buildAutoplay() {
    // Prevent it from building autoplay twice
    if (document.querySelector("ytd-compact-autoplay-renderer") != null) return;

    const watchFlexy = document.querySelector("ytd-watch-flexy");
    const sidebarItems = await getSidebarData();
    const language = yt.config_.HL.split("-")[0] ?? "en";
    const autoplayStub = `
    <ytd-compact-autoplay-renderer class="style-scope ${ sidebarItems.class }">
        <div id="head" class="style-scope ytd-compact-autoplay-renderer">
            <div id="upnext" class="style-scope ytd-compact-autoplay-renderer"></div>
            <div id="autoplay" class="style-scope ytd-compact-autoplay-renderer"></div>
            <tp-yt-paper-toggle-button id="toggle" noink="" class="style-scope ytd-compact-autoplay-renderer" role="button" aria-pressed="" tabindex="0" style="touch-action: pan-y;" toggles="" aria-disabled="false" aria-label="">
                <tp-yt-paper-tooltip id="tooltip" class="style-scope ytd-compact-autoplay-renderer" role="tooltip" tabindex="-1">${ getString("autoplayTip", language) }</tp-yt-paper-tooltip>
            </tp-yt-paper-toggle-button>
        </div>
        <div id="contents" class="style-scope ytd-compact-autoplay-renderer"></div>
    </ytd-compact-autoplay-renderer>
    `;


    // Insert the autoplay stub.
    sidebarItems.element.insertAdjacentHTML("beforebegin", autoplayStub);
    var autoplayRenderer = sidebarItems.element.parentNode.querySelector("ytd-compact-autoplay-renderer");

    // Apply the appropriate localized text.
    autoplayRenderer.querySelector("#upnext").innerText = getString("upNext", language);
    autoplayRenderer.querySelector("#autoplay").innerText = getString("autoplay", language);

    // Add event listener to toggle
    autoplayRenderer.querySelector("#toggle").addEventListener("click", clickAutoplay);

    // Copy first video from data into autoplay renderer
    var firstVideo;
    for (var i = 0; i < sidebarItems.data.length; i++) {
        if (sidebarItems.data[i].compactVideoRenderer) {
            firstVideo = sidebarItems.data[i];
            break;
        }
    }

    var videoRenderer = document.createElement("ytd-compact-video-renderer");
    videoRenderer.data = firstVideo.compactVideoRenderer;
    videoRenderer.classList.add("style-scope", "ytd-compact-autoplay-renderer")
    videoRenderer.setAttribute("lockup", "true");
    videoRenderer.setAttribute("thumbnail-width", "168");
    autoplayRenderer.querySelector("#contents").appendChild(videoRenderer);

    // Add the interval to update toggle if it isn't already.
    if (!watchFlexy.getAttribute("autoplay-interval-active")) {
        setInterval(() => {
            if (autoplayState()) {
                autoplayRenderer.querySelector("#toggle").setAttribute("checked", "");
            } else {
                autoplayRenderer.querySelector("#toggle").removeAttribute("checked");
            }
        }, 100);
    }
}

/**
 * Build new Watch9 elements and tweak currently existing elements accordingly.
 *
 * @returns {void}
 */
 function buildWatch9() {
    const watchFlexy = document.querySelector("ytd-watch-flexy");
    const primaryInfo = watchFlexy.querySelector("ytd-video-primary-info-renderer");
    const secondaryInfo = watchFlexy.querySelector("ytd-video-secondary-info-renderer");
    const viewCount = primaryInfo.querySelector("ytd-video-view-count-renderer");
    const subBtn = secondaryInfo.querySelector("#subscribe-button tp-yt-paper-button");
    const uploadDate = secondaryInfo.querySelector(".date.ytd-video-secondary-info-renderer"); // Old unused element that we inject the date into
    const language = yt.config_.HL.split("-")[0] ?? "en";

    // Let script know we've done this initial build
    watchFlexy.setAttribute("watch9-built", "");

    // Publish date
    var newUploadDate = formatUploadDate(primaryInfo.data.dateText.simpleText, isVideoPublic(), language);
    uploadDate.innerText = newUploadDate;

    // Sub count
    var newSubCount;
    if (secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText) {
        newSubCount = formatSubCount(secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText.simpleText, language);
    } else {
        newSubCount = "0";
    }
    var w9rSubCount = document.createElement("yt-formatted-string");
    w9rSubCount.classList.add("style-scope", "deemphasize");
    w9rSubCount.text = {
        simpleText: newSubCount
    };
    subBtn.insertAdjacentElement("beforeend", w9rSubCount);

    // Bloat buttons
    if (w9rOptions.removeBloatButtons) removeBloatButtons();

    // Autoplay
    if (w9rOptions.oldAutoplay && shouldHaveAutoplay()) buildAutoplay();
}

/**
 * Update currently existing Watch9 elements.
 *
 * @returns {void}
 */
function updateWatch9() {
    const primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
    const secondaryInfo = document.querySelector("ytd-video-secondary-info-renderer");
    const subCnt = secondaryInfo.querySelector("yt-formatted-string.deemphasize");
    const uploadDate = secondaryInfo.querySelector(".date.ytd-video-secondary-info-renderer");
    const language = yt.config_.HL.split("-")[0] ?? "en";

    // Publish date
    var newUploadDate = formatUploadDate(primaryInfo.data.dateText.simpleText, isVideoPublic(), language);
    uploadDate.innerText = newUploadDate;

    // Sub count
    var newSubCount = formatSubCount(secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText.simpleText, language);
    subCnt.text = {
        simpleText: newSubCount
    };

    // Bloat buttons
    if (w9rOptions.removeBloatButtons) removeBloatButtons();

    // Autoplay
    if (w9rOptions.oldAutoplay && shouldHaveAutoplay()) buildAutoplay();
}

/**
 * Run the Watch9 build/update functions.
 */
document.addEventListener("yt-page-data-updated", (e) => {
    if (e.detail.pageType == "watch") {
        if (document.querySelector("ytd-compact-autoplay-renderer")) {
            document.querySelector("ytd-compact-autoplay-renderer").remove();
        }

        if (document.querySelector("ytd-watch-flexy").getAttribute("watch9-built") != null) {
            updateWatch9();
        } else {
            buildWatch9();
        }
    }
});

/**
 * Inject styles.
 */
document.addEventListener("DOMContentLoaded", function tmp() {
    document.head.insertAdjacentHTML("beforeend", `
    <style id="watch9-fix">
    /* Hide Watch11 */
    ytd-watch-metadata {
        display: none !important;
    }

    /* Force Watch10 to display */
    #meta-contents[hidden],
    #info-contents[hidden] {
        display: block !important;
    }

    ytd-video-view-count-renderer[small] {
        font-size: 1.6rem !important;
        line-height: 2.2rem !important;
    }

    yt-formatted-string.deemphasize {
        opacity: .85;
        margin-left: 6px;
    }

    yt-formatted-string.deemphasize:empty {
        margin-left: 0;
    }

    /**
     * Prevent sub count from appearing on the "Edit video" button since
     * it uses the same element as subscribe button
     */
    ytd-button-renderer.style-primary yt-formatted-string.deemphasize {
        display: none;
    }

    #info-strings.ytd-video-primary-info-renderer,
    #owner-sub-count.ytd-video-owner-renderer {
        display: none !important;
    }
    </style>
    `);
    if (w9rOptions.oldAutoplay) document.head.insertAdjacentHTML("beforeend", `
    <style id="compact-autoplay-fix">
    yt-related-chip-cloud-renderer {
        display: none;
    }

    ytd-compact-autoplay-renderer {
        padding-bottom: 8px;
        border-bottom: 1px solid var(--yt-spec-10-percent-layer);
        margin-bottom: 16px;
        display: flex;
        flex-direction: column;
    }

    ytd-compact-autoplay-renderer ytd-compact-video-renderer {
        margin: 0 !important;
        padding-bottom: 8px;
    }

    #head.ytd-compact-autoplay-renderer {
        margin-bottom: 12px;
        display: flex;
        align-items: center;
    }

    #upnext.ytd-compact-autoplay-renderer {
        color: var(--yt-spec-text-primary);
        font-size: 1.6rem;
        flex-grow: 1;
    }

    #autoplay.ytd-compact-autoplay-renderer {
        color: var(--yt-spec-text-secondary);
        font-size: 1.3rem;
        font-weight: 500;
        text-transform: uppercase;
        line-height: 1;
    }

    #toggle.ytd-compact-autoplay-renderer {
        margin-left: 8px;
    }

    ytd-watch-next-secondary-results-renderer #contents.ytd-item-section-renderer > * {
        margin-top: 0 !important;
        margin-bottom: var(--ytd-item-section-item-margin,16px);
    }

    #items.ytd-watch-next-secondary-results-renderer > ytd-compact-video-renderer:first-of-type,
    ytd-watch-next-secondary-results-renderer #contents.ytd-item-section-renderer > ytd-compact-video-renderer:first-of-type {
        display: none !important;
    }
    </style>
    `);
    document.removeEventListener("DOMContentLoaded", tmp);
});