Watch9 Reconstruct

Restores the old watch layout from before 2019

// ==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);
});