YouTube Similar Comments Hider

Ensure originality in YouTube's comment section by hiding all sorts of repeated comments, copy-paste comments, repeated quotes from the video and saturated memes.

目前为 2021-11-30 提交的版本。查看 最新版本

作者
hjk789
评分
0 0 0
版本
1.4
创建于
2021-10-15
更新于
2021-11-30
大小
17.1 KB
许可证
https://github.com/hjk789/Userscripts/tree/master/YouTube-Similar-Comments-Hider#license
适用于

// ==UserScript== // @name YouTube Similar Comments Hider // @version 1.4 // @description Ensure originality in YouTube's comment section by hiding all sorts of repeated comments, copy-paste comments, repeated quotes from the video and saturated memes. // @author BLBC (github.com/hjk789, greasyfork.org/users/679182-hjk789) // @copyright 2021+, BLBC (github.com/hjk789, greasyfork.org/users/679182-hjk789) // @homepage https://github.com/hjk789/Userscripts/tree/master/YouTube-Similar-Comments-Hider // @license https://github.com/hjk789/Userscripts/tree/master/YouTube-Similar-Comments-Hider#license // @match https://www.youtube.com/watch* // @grant GM.getValue // @grant GM.setValue // ==/UserScript==

//--------------- SETTINGS ---------------

const tolerance = 3 // 1 - Loosely similar: Pretty much all similar comments will be detected, but there will be many false positives. False positives are comments that are worded similarly but have two totally different subjects. // 2 - Significantly similar: Most similar comments will be detected, but with some or few false positives. // 3 - Very similar: A moderate detection with few to no false positives, but several comments that are similar, but worded differently, won't be detected. // 4 - Mostly similar: Detects comments that are close variations of another, such as several comments repeating the same quote from the video with few differences. // 5 - Almost identical: Detects only comments that are mostly copy-pasted with little to no variation.

let lightenSimilarComments = false // If set to true, all similar comments will be dimmed (faded) instead of completely hidden.

//----------------------------------------

let treshold = getTreshold(tolerance) let tolerance4 = getTreshold(4) + 10, tolerance5 = getTreshold(5)

let samples = []

let blockedUsers, selectedUser, blockUserContainer

GM.getValue("blockedUsers").then(function(value) { if (!value) { value = "[]" GM.setValue("blockedUsers", value) }

blockedUsers = JSON.parse(value)

})

const waitForCommentSection = setInterval(function() { let commentSection = document.getElementById("comments")?.querySelector("#contents")

if (!commentSection)
    return

clearInterval(waitForCommentSection)


/* Attach a mutation observer to the comments section to detect when more comments are loaded, and process them */
{
    const loadedCommentsObserver = new MutationObserver(function(mutations)
    {
        for (let i=0; i < mutations.length; i++)
        {
            if (!!mutations[i].addedNodes)
                processComments(mutations[i].addedNodes)
        }
    })

    loadedCommentsObserver.observe(commentSection, {childList: true})
}


const waitForCommentSectionHeader = setInterval(function()
{
    if (!document.getElementById("sort-menu"))
        return

    clearInterval(waitForCommentSectionHeader)


    /* Create the hover styles for the menu items */
    {
        const style = document.createElement("style")
        style.innerHTML = "#toleranceMenu div div div:hover, #blockUser:hover { background-color: #e7e7e7 !important; }"
        document.head.appendChild(style)
    }

    /* Create the "Filter tolerance" dropdown menu */
    {
        const toleranceMenuContainer = document.createElement("div")
        toleranceMenuContainer.id = "toleranceMenu"
        toleranceMenuContainer.innerHTML = "FILTER TOLERANCE"
        toleranceMenuContainer.style = "width: 130px; height: 24px; margin-left: 50px; font-size: 14px; font-weight: 500; z-index: 99; cursor: pointer;"
        toleranceMenuContainer.onclick = function() { this.lastChild.style.visibility = this.lastChild.style.visibility ? "" : "hidden"; event.stopPropagation() }

        const dropdownContainer = document.createElement("div")
        dropdownContainer.style = "background-color: white; width: max-content; margin-left: -15px; margin-top: 16px; border: lightgray 1px solid; border-radius: 3px; visibility: hidden;"

        var dropdownItemsContainer = document.createElement("div")
        dropdownItemsContainer.style = "font-weight: initial; letter-spacing: 0.3px; padding-top: 7px;"

        createToleranceDropdownItem("Loosely similar", 1, dropdownItemsContainer, "Pretty much all similar comments will be detected, but there will be many false positives.")
        createToleranceDropdownItem("Significantly similar", 2, dropdownItemsContainer, "Most similar comments will be detected, but with some or few false positives.")
        createToleranceDropdownItem("Very similar", 3, dropdownItemsContainer, "A moderate detection with few to no false positives, but several comments that are similar, but worded differently, won't be detected.")
        createToleranceDropdownItem("Mostly similar", 4, dropdownItemsContainer, "Detects comments that are close variations of another.")
        createToleranceDropdownItem("Almost indentical", 5, dropdownItemsContainer, "Detects only comments that are mostly copy-pasted with little to no variation.")

        dropdownContainer.appendChild(dropdownItemsContainer)

        toleranceMenuContainer.appendChild(dropdownContainer)

        document.getElementById("sort-menu").parentElement.appendChild(toleranceMenuContainer)

        document.body.onclick = function() { document.getElementById("toleranceMenu").lastChild.style.visibility = "hidden" }               // Make the dropdown be dismissed when clicked outside of it.
    }


    /* Create the "Hide comments" checkbox */
    {
        const hideCommentsCheckbox = document.createElement("input")
        hideCommentsCheckbox.id = "hideComments"
        hideCommentsCheckbox.type = "checkbox"
        hideCommentsCheckbox.style = "margin-top: 10px; margin-bottom: 10px;"
        hideCommentsCheckbox.checked = !lightenSimilarComments
        hideCommentsCheckbox.onchange = function()
        {
            lightenSimilarComments = !this.checked

            if (this.checked)
            {
                const comments = document.getElementById("comments").querySelectorAll("ytd-comment-thread-renderer[style^='opacity']")

                for (let i=0; i < comments.length; i++)
                    comments[i].style = "display: none;"
            }
            else
            {
                const comments = document.getElementById("comments").querySelectorAll("ytd-comment-thread-renderer[style^='display']")

                for (let i=0; i < comments.length; i++)
                    comments[i].style = "opacity: 0.5;"
            }

        }

        const hideCommentsLabel = document.createElement("label")
        hideCommentsLabel.for = "hideComments"
        hideCommentsLabel.style = "padding: 8px 19px; border-top: 1px solid; user-select: none;"
        hideCommentsLabel.innerHTML = "Hide comments"

        hideCommentsLabel.insertBefore(hideCommentsCheckbox, hideCommentsLabel.firstChild)

        dropdownItemsContainer.appendChild(hideCommentsLabel)
    }


    /* Create the "Block this user" option in the comment's side-menu */
    {
        blockUserContainer = document.createElement("div")
        blockUserContainer.id = "blockUser"
        blockUserContainer.style = "background-color: white; font-size: 14px; text-align: center; padding: 8px 0px 8px 0px; cursor: pointer; margin-bottom: 8px;"
        blockUserContainer.innerHTML = "Block this user"
        blockUserContainer.onclick = function()
        {
            if (confirm("This will hide all comments from ''"+selectedUser.innerText.trim()+"'' in any video. Are you sure?"))
            {
                blockedUsers.push(selectedUser.href)
                GM.setValue("blockedUsers", JSON.stringify(blockedUsers))
                reprocessComments()
                document.body.click()     // Dismiss the menu.
            }
        }

        const blockUserIcon = document.createElement("span")
        blockUserIcon.innerHTML = "🚫"
        blockUserIcon.style = "margin-right: 17px; margin-left: 10px;"

        blockUserContainer.insertBefore(blockUserIcon, blockUserContainer.firstChild)

        const commentMenuButton = commentSection.querySelector("ytd-menu-renderer yt-icon")
        commentMenuButton.click()
        commentMenuButton.click()      // The comment menu doesn't exist in the HTML before it's clicked for the first time. This forces it to be created and dismisses it immediately.

        const blockUserParent = document.querySelector("ytd-menu-popup-renderer")
        blockUserParent.style = "max-height: max-content !important; max-width: max-content !important;"                // Change the max width and height so that the new item fits in the menu.
    }

    document.body.onclick = function()
    {
        document.getElementById("blockUser")?.remove()                // Remove the "Block this user" option when not used.

        const toleranceMenu = document.getElementById("toleranceMenu")
        if (toleranceMenu)  toleranceMenu.firstElementChild.style.visibility = "hidden"              // Dismiss the Filter Tolerance menu when clicked outside.
    }


}, 100)

}, 100)

function getTreshold(tolerance) { // Return the minimum threshold to treat the comment as similar, depending on the tolerance level. The final threshold can be bigger than that, but not lower. return tolerance == 1 ? 14 : tolerance == 2 ? 24 : tolerance == 3 ? 35 : tolerance == 4 ? 45 : 65 }

function createToleranceDropdownItem(text, toleranceLevel, container, title) { const item = document.createElement("div") item.innerHTML = text item.title = title item.style.padding = "15px" item.onclick = function() { this.parentElement.querySelector("[style*='background-color']").style.backgroundColor = "" // Remove the selection style from the previous selected item. this.style.backgroundColor = "#e7e7e7" this.parentElement.parentElement.style.visibility = "hidden !important" // Hide the dropdown list when an item is selected.

    reprocessComments(getTreshold(toleranceLevel))
}

if (tolerance == toleranceLevel)
    item.style.backgroundColor = "#e7e7e7"

container.appendChild(item)

}

function reprocessComments(tresholdValue = 0) { if (tresholdValue) treshold = tresholdValue

const comments = document.getElementById("comments").querySelector("#contents").children

processComments(comments, true)

}

function processComments(comments, reprocess = false) { for (let i=0; i < comments.length; i++) { const commentBody = comments[i].querySelector("#content-text") if (!commentBody) // Sometimes the comments list includes an empty object. When it's such a case, skip to the next one. continue

    // Because the comment's side-menu is separated from the comments section, this listens to clicks on each three-dot
    // button and store in a variable in what comment it was clicked, to then be used by the "Block this user" button.
    const commentMenuButton = comments[i].querySelector("ytd-menu-renderer")
    if (commentMenuButton)
    {
        commentMenuButton.onclick = function()
        {
            event.stopPropagation()             // Prevent the "Block this user" option from being removed by clicking the comment menu.

            selectedUser = this.parentElement.parentElement.querySelector("#author-text")

            document.querySelector("ytd-menu-popup-renderer").appendChild(blockUserContainer)               // YouTube reuses the same menu element for every menu in the site. This adds the "Block this user" option
        }                                                                                                   // to the menu only when the comment menu is opened. It's then removed whenever any other menu is opened.
    }

    // Standardize the comments for the processing by making them lowercase and without punctuation marks, diacritics, linebreaks or
    // characters repeated more than twice, so that the differences between comments are in the words used instead of the characters.
    const comment = commentBody.textContent.toLocaleLowerCase().replace(/[.,!\-\n]/g, " ").replace(/ +/g, " ").replace(/(.)\1+/gu, "$1").replace(/(👏|🤩|😁|😍|❤️|👍🏼|💯|👊🏻)+/g, "EMJ").normalize("NFD").replace(/[\u0300-\u036f*"'’“”]/g, "").trim()

    if (!reprocess)             // If it's a reprocess, don't add the comment again to the samples list, otherwise the list would get duplicated.
        samples.push(comment)
    else
    {
        // Reset the style of the filtered comments
        if (comments[i].style.opacity || comments[i].style.display)
            comments[i].removeAttribute("style")
    }

    if (blockedUsers.includes(comments[i].querySelector("#author-text").href))              // The check need to be made *after* the push, otherwise the comments list and the samples list get out of sync.
    {
        comments[i].style.display = "none"
        continue
    }

    let n = samples.length
    if (!reprocess)  n--                // The first time the processing is done, the comment should not be compared to the sample added last, as it would be comparing to itself ...

    /* Compare the comment with the previous ones */

    for (let j=0; j < n; j++)
    {
        if (reprocess && i == j)       // ... On the other hand, in the reprocessings, the comparison should stop on equal indexes to not compare to itself.
            break

        const sample = samples[j]

        const similarity1 = calculateSimilarity(sample, comment)

        const lengthSum = comment.length + sample.length

        let tmpTreshold = lengthSum * treshold/100              // The length of both comments is connected to the minimum threshold, this way the threshold is adapted to each comparison.

        if (lengthSum/100 < 1)
            tmpTreshold /= lengthSum/100

        if (treshold < tolerance5)              // The tolerance 5 is much stricter than the other tolerances, so it can't be treated as the limit for normal tolerances, and must instead be treated separately.
        {
            if (tmpTreshold > tolerance4)              // Don't let the final threshold be too high, otherwise several long similar comments wouldn't be detected.
                tmpTreshold = tolerance4
        }
        else if (tmpTreshold > tolerance5)
                tmpTreshold = tolerance5


        if (similarity1 >= tmpTreshold)
        {
            const similarity2 = calculateSimilarity(comment, sample)                // Recalculate the other way round to ensure that the two comments are similar to each other in both ways.

            if (similarity2 >= tmpTreshold)
            {
                console.log("Similarity C->S: "+similarity1.toFixed(2)
                            +"   ###   Similarity S->C: "+similarity2.toFixed(2)
                            +"   ###   Treshold: "+tmpTreshold.toFixed(2)
                            +"   ###   C length: "+comment.length
                            +"   ###   S length: "+sample.length
                            +"   ###   Sample: "+sample
                            +"   ###   Comment: "+comment)

                if (lightenSimilarComments)
                    comments[i].style.opacity = 0.5
                else
                    comments[i].style.display = "none"

                break
            }
        }
    }
}

}

function calculateSimilarity(a, b) { let hits = 0 let string = ""

for (let i=0; i < b.length; i++)                // For each character of the comment ...
{
    string += b[i]                              // ... append it to a string ...

    if (a.includes(string))                     // ... and check if the resulting string can be found in the sample comment, and if so, continue appending the characters.
    {
        if (string.length > 2)                  // When the sample comment contains the string, when it's at least 3 characters long ...
        {
            hits++                              // ... start counting the number of hits for each character.

            if (string.length == 3)             // If the string has three characters, recover the two uncounted hits.
                hits += 2
        }
    }
    else string = ""                            // If the comment doesn't contain the string, clear the string and start building it again with the rest of the characters.
}

const similarity = hits/b.length*100            // Get the proportion of hits out of the total of characters of the comment.

return similarity

}