Greasy Fork is available in English.

Youtube留言黑名單

屏蔽黑名單內頻道在其他影片下的留言,可以查看和移除黑名單內的頻道。

// ==UserScript==
// @name         Youtube留言黑名單
// @namespace    http://tampermonkey.net/
// @version      1.6.1
// @description  屏蔽黑名單內頻道在其他影片下的留言,可以查看和移除黑名單內的頻道。
// @author       Microdust
// @match        https://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// @grant        GM_getResourceText
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11.0.18/dist/sweetalert2.all.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    //以下設定將在重載網頁後生效

    //是否刪除在黑名單內的留言true=刪除/false=不刪除留言但用deleteText裡的文字覆蓋
    const deleteComment = true;
    //不刪除留言時用deleteText裡的文字覆蓋
    const deleteText = "留言被屏蔽";
    //#黑名單-導出的檔名(不可為空)
    const exportName = "黑名單";
    //#黑名單-佔整個畫面的寬度比例
    const blacklistWidth = "50%";
    //#黑名單-名字顯示最長字數(超過此限制將會以'...'省略,數字為負將不限制)
    const nameLength = -1;
    //留言最小字數過濾,小於0則不限制(需小於留言最大字數)
    const commentMinLength = 0;
    //留言最大字數過濾,小於0則不限制(需大於留言最小字數)
    const commentMaxLength = 0;
    //關鍵字過濾
    const banWords = [
        //    "將要過濾的關鍵字填入雙引號中","刪除註解以啟用過濾","可自由增減關鍵字"
    ];
    //
    //以下為範例:
    //---------------------------------
    //-   const banWords=[
    //-       "關","鍵字","範例"
    //-   ];
    //---------------------------------
    //
    let btnSetting;
    let prelink;
    let link;
    let thisPath;
    let text;
    let viewURL;
    let workArea = true;
    let list = [];
    //
    let checkedComment;
    let path;
    let comment;
    let commentUserId;
    let commentUserName;
    let commentValue;
    let btnBlackList;
    let precomment = [];
    //
    let threadPath;
    let checkedReply;
    let replyPath;
    let reply;
    let replyUserId;
    let replyUserName;
    let replyValue;
    let replyBtn;
    let replyCount = [];
    let replyBan = [];
    let noReply = [];

    if (GM_getValue("list")) list = blacklist("init");
    //list.length=0;
    //blacklist("save");

    setInterval(function() {
        link = window.location.href;
        if (prelink != link) {
            prelink = window.location.href;
            workArea = true;
            checkedComment = 0;
            viewURL = false;
            noReply.length = 0;
        }
        if (workArea) workArea = fnNameListCheck();
    }, 1000);

    function fnNameListCheck() {
        let pathName = window.location.pathname.split('/');
        if (!viewTypeFn(pathName, window.location.search.toString())) return false;
        if (checkedComment) btnSettingFn();
        while (getComment()) {
            defData();
            checkedComment++;
        }
        getReplyExist();
        return true;
    }

    function viewTypeFn(viewType, val) {
        if (viewType[1] == "watch") viewURL = "ytd-watch-flexy/div[5]/div[1]/div/div[2]";
        if (viewType[1] == "post") viewURL = "/ytd-browse/ytd-two-column-browse-results-renderer/div[1]/ytd-section-list-renderer/div[2]";
        if (viewType[3] == "community" && val != "") viewURL = "/ytd-browse/ytd-two-column-browse-results-renderer/div[1]/ytd-section-list-renderer/div[2]";
        threadPath = "/html/body/ytd-app/div[1]/ytd-page-manager/" + viewURL + "/ytd-comments/ytd-item-section-renderer/div[3]";
        return viewURL;
    }

    function btnSettingFn() {
        btnSetting = getElementByXpath("/html/body/ytd-app/div[1]/ytd-page-manager/" + viewURL + "/ytd-comments/ytd-item-section-renderer/div[1]/ytd-comments-header-renderer/div[5]/ytd-comment-simplebox-renderer/yt-img-shadow/img");
        //console.log(btnSetting);
        if (!btnSetting) return;
        var oBlackList = document.createElement("div");
        for (let i = 0; i < list.length; i++) {
            var banner = document.createElement('button');
            banner.onclick = function() {
                let id = list[i].id;
                let name = list[i].name;
                wordPure(name);
                Swal.fire({
                    title: '確定要將 ' + name + ' 從黑名單移除嗎?',
                    text: '頻道ID(' + id + ')',
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: '從黑名單移除',
                    cancelButtonText: '取消'
                }).then((result) => {
                    if (result.isConfirmed) {
                        Swal.fire(
                            '已將 ' + name + ' 從黑名單移除',
                            '頻道ID(' + id + ')',
                            'info'
                        ).then(() => {
                            list.splice(banlistSearch(id) - 1, 1);
                            //list.length = 0;
                            blacklist("save");
                        })
                    }
                })
            }
            banner.innerText = wordPure(list[i].name) + " (" + list[i].id + ")";
            let br = document.createElement('br');
            oBlackList.append(banner);
            oBlackList.append(br);
        }
        if (list.length == 0) {
            banner = document.createElement('p');
            banner.innerText = "沒有任何人被你列入黑名單";
            oBlackList.append(banner);
        }
        btnSetting.onclick = function() {
            Swal.fire({
                title: '黑名單',
                width: blacklistWidth,
                html: this.appendChild(oBlackList),
                confirmButtonText: '確認',
                showDenyButton: true,
                denyButtonText: '導出/導入',
                backdrop: 'rgba(0,0,0,0.6)'
            }).then((result) => {
                if (result.isDenied) {
                    Swal.fire({
                        title: '黑名單(導出/導入)',
                        icon: 'question',
                        confirmButtonText: '導入',
                        showDenyButton: true,
                        denyButtonText: '導出',
                        backdrop: 'rgba(0,0,0,0.6)'
                    }).then((result) => {
                        if (result.isConfirmed) {
                            importFn();
                        } else if (result.isDenied) {
                            exportFn(GM_getValue("list"), exportName + ".json");
                        }
                    })
                }
            })
        }
    }

    function getReplyExist() {
        for (let i = 1; i < checkedComment + 1; i++) {
            if (noReply[i]) continue;
            let oreplyCountMain = getElementByXpath(threadPath + "/ytd-comment-thread-renderer[" + i + "]/div/ytd-comment-replies-renderer/div[1]/div[1]/div[1]/ytd-button-renderer/a/tp-yt-paper-button/yt-formatted-string");
            let oreplyCount = oreplyCountMain;
            if (!(oreplyCountMain && !oreplyCount)) {
                if (!oreplyCount) noReply[i] = true;
                if (!oreplyCount) continue;
                if (parseInt(oreplyCount.textContent) === replyCount[i]) noReply[i] = true;
                if (parseInt(oreplyCount.textContent) === replyCount[i]) continue;
            } else {
                if (replyCount[i] === 1) noReply[i] = true;
                if (replyCount[i] === 1) continue;
            }
            checkedReply = 1;
            if (replyCount[i] > 0 && replyCount[i] % 10 === 0) checkedReply = replyCount[i] + 1;
            while (getReply(i)) {
                foldedData(i);
                checkedReply++;
            }
            replyCount[i] = checkedReply - 1;
        }
    }

    function getReply(thread) {
        replyPath = threadPath + "/ytd-comment-thread-renderer[" + thread + "]/div/ytd-comment-replies-renderer/div[1]/div[2]/div[1]/ytd-comment-renderer[" + checkedReply + "]";
        reply = document.evaluate(replyPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        return reply;
    }

    function foldedData(thread) {
        replyUserId = getElementByXpath(replyPath + "/div[3]/div[2]/div[1]/div[2]/h3/a").href.toString().replace(/https:\/\/www.youtube.com\/channel\//g, "");
        replyUserName = getElementByXpath(replyPath + "/div[3]/div[2]/div[1]/div[2]/h3/a/span");
        if (replyUserName) replyUserName = replyUserName.textContent.toString().replace(/\n              /g, "").replace(/\n            /g, "");
        replyValue = getElementByXpath(replyPath + "/div[3]/div[2]/div[2]/ytd-expander/div/yt-formatted-string");
        btnBlackList = getElementByXpath(replyPath);
        btnBlackList.setAttribute("data-commentID", replyUserId);
        btnBlackList.setAttribute("data-commentName", replyUserName);
        btnBlackList.setAttribute("data-commentSeq", thread);
        if (banlistSearch(replyUserId) || wordFilter(replyValue.textContent)) {
            if (reply && deleteComment) reply.style.display = 'none';
            if (replyValue) {
                replyValue.innerText = deleteText;
                replyValue.setAttribute("style", "font-style: italic;");
            }
        }
        btnBlackList.ondblclick = function() {
                banCheck(
                    this.getAttribute('data-commentID'),
                    this.getAttribute('data-commentName'),
                    this.getAttribute('data-commentSeq'),
                    true
                );
            }
            //console.log(checkedReply,replyUserName,"reply");
    }

    function getComment() {
        path = "/html/body/ytd-app/div[1]/ytd-page-manager/" + viewURL + "/ytd-comments/ytd-item-section-renderer/div[3]/ytd-comment-thread-renderer[" + (checkedComment + 1) + "]";
        comment = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        if (comment === precomment[(checkedComment + 1)]) return false;
        precomment[(checkedComment + 1)] = comment
        return comment;
    }

    function defData() {
        commentUserId = getElementByXpath(path + "/ytd-comment-renderer/div[3]/div[2]/div[1]/div[2]/h3/a").href.toString().replace(/https:\/\/www.youtube.com\/channel\//g, "");
        commentUserName = getElementByXpath(path + "/ytd-comment-renderer/div[3]/div[2]/div[1]/div[2]/h3/a/span");
        if (commentUserName) commentUserName = commentUserName.textContent.toString().replace(/\n              /g, "").replace(/\n            /g, "");
        commentValue = getElementByXpath(path + "/ytd-comment-renderer/div[3]/div[2]/div[2]/ytd-expander/div/yt-formatted-string");
        //console.log(commentValue);
        btnBlackList = getElementByXpath(path + "/ytd-comment-renderer");
        btnBlackList.setAttribute("data-commentID", commentUserId);
        btnBlackList.setAttribute("data-commentName", commentUserName);
        btnBlackList.setAttribute("data-commentSeq", checkedComment + 1);
        if (banlistSearch(commentUserId) || wordFilter(commentValue.textContent)) {
            if (comment && deleteComment) comment.style.display = 'none';
            if (commentValue) {
                commentValue.innerText = deleteText;
                commentValue.setAttribute("style", "font-style: italic;");
            }
        }
        btnBlackList.ondblclick = function() {
                banCheck(
                    this.getAttribute('data-commentID'),
                    this.getAttribute('data-commentName'),
                    this.getAttribute('data-commentSeq')
                );
            }
            //console.log(checkedComment,commentUserName);
    }

    function banCheck(id, name, seq, fold) {

        thisPath = "/html/body/ytd-app/div/ytd-page-manager" + viewURL + "/ytd-comments/ytd-item-section-renderer/div[3]/ytd-comment-thread-renderer[" + seq + "]";
        if (!fold) text = getElementByXpath(thisPath + "/ytd-comment-renderer/div[3]/div[2]/div[2]/ytd-expander/div/yt-formatted-string");
        if (fold) text = false;
        wordPure(name);
        if (!banlistSearch(id)) {
            Swal.fire({
                title: '確定要將 ' + name + ' 列入黑名單嗎?',
                text: '頻道ID(' + id + ')',
                icon: 'warning',
                showCancelButton: true,
                confirmButtonColor: '#3085d6',
                cancelButtonColor: '#d33',
                confirmButtonText: '列入黑名單',
                cancelButtonText: '取消'
            }).then((result) => {
                if (result.isConfirmed) {
                    Swal.fire(
                        '已將 ' + name + ' 列入黑名單',
                        '頻道ID(' + id + ')',
                        'info'
                    ).then(() => {
                        list.unshift({ "id": id, "name": name });
                        //list.length = 0;
                        blacklist("save");
                        if (text) {
                            let info = document.createElement('span');
                            text.innerText = "";
                            while (text.length > 0) {
                                text.pop();
                            }
                            info.innerText = "留言將在之後屏蔽";
                            info.setAttribute("style", "font-style: italic;color:red;");
                            text.append(info);
                        } else {
                            replyCount[seq] = 0;
                            noReply[seq] = false;
                        }
                    })
                }
            })
        } else {
            Swal.fire({
                title: '確定要將 ' + name + ' 從黑名單移除嗎?',
                text: '頻道ID(' + id + ')',
                icon: 'warning',
                showCancelButton: true,
                confirmButtonColor: '#3085d6',
                cancelButtonColor: '#d33',
                confirmButtonText: '從黑名單移除',
                cancelButtonText: '取消'
            }).then((result) => {
                if (result.isConfirmed) {
                    Swal.fire(
                        '已將 ' + name + ' 從黑名單移除',
                        '頻道ID(' + id + ')',
                        'info'
                    ).then(() => {
                        list.splice(banlistSearch(id) - 1, 1);
                        //list.length = 0;
                        blacklist("save");
                        if (text) {
                            let info = document.createElement('span');
                            text.innerText = "";
                            while (text.length > 0) {
                                text.pop();
                            }
                            info.innerText = "此留言者已從黑名單解封,之後就能再次看見";
                            info.setAttribute("style", "font-style: italic;color:orange;");
                            text.append(info);
                        }
                    })
                }
            })
        }
    }

    function blacklist(event) {
        if (event == "save") return GM_setValue("list", JSON.stringify(list));
        return JSON.parse(GM_getValue("list"));
    }

    function banlistSearch(id) {
        for (let i = 0; i < list.length; i++) {
            if (list[i].id == id) {
                return i + 1;
                break;
            }
        }
    }

    function exportFn(content, filename) {
        let odownload = document.createElement("a");
        odownload.download = filename;
        odownload.style.display = "none";
        let jsonBlob = new Blob([encodeURI(content, "utf-8")], { type: "text/plain;charset=utf-8" });
        odownload.href = URL.createObjectURL(jsonBlob);
        document.body.appendChild(odownload);
        odownload.click();
        document.body.removeChild(odownload);
    }

    function importFn() {
        let oimport = document.createElement("input");
        oimport.style.display = "none";
        oimport.type = "file";
        oimport.accept = ".json";
        oimport.onchange = function() {
            if (oimport.files.length != 0 && oimport.files[0].type.match(/json.*/)) {
                let reader = new FileReader();
                reader.onload = function(e) {
                    let loadData = JSON.parse(decodeURIComponent(e.target.result));
                    Swal.fire({
                        title: '導入黑名單',
                        text: "請選擇要對新資料的處理方式",
                        confirmButtonText: '和原資料合併',
                        showDenyButton: true,
                        denyButtonText: '覆蓋原資料',
                        backdrop: 'rgba(0,0,0,0.6)'
                    }).then((result) => {
                        if (result.isDenied) {
                            list = loadData;
                            blacklist("save");
                            Swal.fire('已覆蓋原資料');
                        } else if (result.isConfirmed) {
                            for (let i = 0; i < loadData.length; i++) {
                                if (!list.find(element => element.id === loadData[i].id)) {
                                    //console.log(loadData[i]);
                                    list.unshift(loadData[i]);
                                }
                            }
                            blacklist("save");
                            Swal.fire('已合併兩資料');
                        }
                    })
                    reader.onerror = function(e) {
                        Swal.fire('無法讀取檔案');
                    }
                }
                reader.readAsText(oimport.files[0], "ISO-8859-1");
            } else {
                Swal.fire('上傳的檔案非json檔');
            }
        }
        oimport.click();
    }

    function wordFilter(commentText) {
        if (commentMinLength > 0 && commentText.length < commentMinLength) return commentText.length;
        if (commentMaxLength > 0 && commentText.length > commentMaxLength) return commentText.length;
        if (banWords.length <= 0) return false;
        for (let i = 0; i < banWords.length; i++) {
            if (commentText.indexOf(banWords[i]) + 1) return banWords[i];
        }
    }

    function wordPure(word) {
        if (nameLength >= 0 && word.length > nameLength) {
            return word.substring(0, nameLength) + "...";
        } else {
            return word;
        }
    }

    function getElementByXpath(paths) {
        return document.evaluate(paths, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }
})();