// ==UserScript==
// @name Bitchute - Extras
// @author "Dilxe"
// @namespace https://github.com/Dilxe/
// @version 0.85.3
// @description Adds extra functionality to Bitchute, such as: mark watched videos and allow to block comments based on content and/or username. v0.7 2021-07-15
// @description Comment blocking currently can only be done by adding keywords or usernames to the RegEx lists below. A front-end method'll be added later on.
// @match https://www.bitchute.com/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.getResourceUrl
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js
// @require https://gf.qytechs.cn/scripts/31940-waitforkeyelements/code/waitForKeyElements.js?version=209282
// @run-at document-idle
// ==/UserScript==
// Others //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Mutation Observer ------------------------------------------------------------------------------------------------------------------------------
//// Dynamically detects changes to the HTML. It's used here to, when the user scrolls through the videos list, mark the older videos as they load.
//// Source: https://stackoverflow.com/a/11546242
function detectMutation()
{
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(
function(mutations, observer)
{
// fired when a mutation occurs
//console.log(mutations, observer);
markWatchedVideos();
// ...
});
var config = { attributes: false, childList: true, characterData: false, subtree: false };
// define what element should be observed by the observer
// and what types of mutations trigger the callback
if(document.URL.includes('channel') == true)
{
observer.observe(document.getElementsByClassName("channel-videos-list")[0], config);
}
else if (document.URL.length <= 25)
{
observer.observe(document.getElementsByClassName("row auto-clear")[1], config);
}
}
// Detect Click on video ---------------------------------------------------------
//document.querySelectorAll("a[href*='/video/']"); // query src = https://stackoverflow.com/a/37820644 // wildcard = https://stackoverflow.com/a/8714421
// Detect URL Change ---------------------------------------------------------
//// This is to avoid having to refresh the page (F5) or open in a new window.
/*--- Note, gmMain () will fire under all these conditions:
1) The page initially loads or does an HTML reload (F5, etc.).
2) The scheme, host, or port change. These all cause the browser to
load a fresh page.
3) AJAX changes the URL (even if it does not trigger a new HTML load).
Source: https://stackoverflow.com/a/18997637
*/
var fireOnHashChangesToo = true;
var pageURLCheckTimer = setInterval (
function ()
{
if (this.lastPathStr !== location.pathname || this.lastQueryStr !== location.search || (fireOnHashChangesToo && this.lastHashStr !== location.hash))
{
this.lastPathStr = location.pathname;
this.lastQueryStr = location.search;
this.lastHashStr = location.hash;
// [For Debugging] - If the message (of the amount of removed comments) exists, it'll be removed.
if(document.getElementById('div-debug') != null)
{
document.getElementById('div-debug').remove();
document.getElementById('btn-debug').remove();
}
// Background div for videos list & removed comments
const divDebugElementStyle = "display:none; position:absolute; top:69%; left:19.9%; width:60.1vw; height:30vw; background-color:#211f22; " +
"color:#908f90; text-align:center; z-index:inherit;";
// Background div and btn for textarea & removed comments
elementForDataDisplay( "", "div", "div-debug", divDebugElementStyle, 'nav-top-menu' );
window.addEventListener ("hashchange", gmMain, false);
document.addEventListener("visibilitychange", gmMain); //https://stackoverflow.com/questions/1760250/how-to-tell-if-browser-tab-is-active#comment111113309_1760250
gmMain();
}
} , 111);
function gmMain()
{
if (document.getElementById("loader-container").style.display == "none")
{
if(document.URL.includes('video') == true)
{
/* waitForKeyElements() - Needs jQuery.
It's used for the same reasons one would use setTimeout, while being more exact due to only running the code after the chosen elements are loaded.
Source: https://stackoverflow.com/a/17385193 // https://stackoverflow.com/questions/16290039/script-stops-working-after-first-run */
waitForKeyElements ( "#comment-list", filterComments );
waitForKeyElements ( ".sidebar-video", markWatchedVideos() );
}
else if(document.URL.includes('channel') == true)
{
detectMutation();
waitForKeyElements ( "#channel-videos", markWatchedVideos() );
}
else
{
detectMutation();
waitForKeyElements ( ".row.auto-clear", markWatchedVideos() );
}
}
else
{
setTimeout(gmMain, 1000);
}
}
// Main //
// Mark Watched Videos ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async function markWatchedVideos()
{
let allVideos = []; // Creates an array to put all the videos on the page. Needed because channel videos have different classes.
const globalVideos = document.getElementsByClassName('video-card'); // Fetches the videos from the element
const channelVideos = document.getElementsByClassName('channel-videos-image-container'); // Same as above but for the channels
Array.prototype.push.apply(allVideos, Array.from(globalVideos)); // Add the videos to the array
Array.prototype.push.apply(allVideos, Array.from(channelVideos)); // Same as above but for the channels
let totalNumVideos = allVideos.length; // Counts the total nº of videos
let watchedVideos = await GM.getValue("videoHREF"); // Loads the list of watched videos
//removeVideos = '';
//saveList = await GM.setValue("videoHREF", removeVideos);
// Checks the video count. If bigger than X threshold, remove an older video.
if (watchedVideos.split("|").length > 4000)
{
let olderVideoRemoved = watchedVideos.replace("|" + watchedVideos.split("|")[1],"");
await GM.setValue("videoHREF", olderVideoRemoved);
}
// Checks if it's a video page, has been watched and, therefore on GM(GreaseMonkey)'s list. If it's not, gets added.
if (document.URL.includes('video') == true && watchedVideos.indexOf(document.baseURI.split("/")[4]) == -1)
{
let markCurrentVideo = watchedVideos + '|' + document.baseURI.split("/")[4];
await GM.setValue("videoHREF", markCurrentVideo);
}
// Checks video by video whether they're watched, if so, marks.
for (videoNum = 0; videoNum < totalNumVideos; videoNum++)
{
// If it's in the list, add CSS
if (watchedVideos.match(allVideos[videoNum].children[0].pathname.slice(7,allVideos[videoNum].children[0].pathname.length-1)) != null)
{
//alert('Parsing: ' + videoNum + '/' + totalNumVideos);
const div = document.createElement('thumbnailOverlay');
div.style.background = '#2c2a2d';
div.style.borderRadius = '2px';
div.style.color = '#908f90';
div.innerHTML = 'WATCHED';
div.style.fontSize = '11px';
div.style.right = '4px';
//div.style.opacity = '0.8';
div.style.padding = '3px 4px 3px 4px';
div.style.position = 'absolute';
div.style.top = '4px';
div.style.fontFamily = 'Roboto, Arial, sans-serif';
if (allVideos[videoNum].className == "video-card")
{
document.getElementsByClassName('video-card-image')[videoNum].style.opacity = '0.25';
}
else if (allVideos[videoNum].className == "channel-videos-image-container")
{
document.getElementsByClassName('channel-videos-image')[videoNum-globalVideos.length].style.opacity = '0.25';
}
allVideos[videoNum].appendChild(div);
}
}
// [For Debugging] //////////////////////////////////////////////////////////////////////////////////////////
if(document.getElementById("txt-marked-videos-list") == null)
{
// Debug Button
const btnMkdVidsInnerTxt = 'Debug Menu';
const btnMkdVidsId = "btn-debug";
const btnMkdVidsStyle = "position:absolute; top:29%; right:25%; background-color:#211f22; color:#908f90; font-size: 0.9vw; width: 12vw; height: 1.9vw;";
btnForDataDisplay(btnMkdVidsInnerTxt,btnMkdVidsId,btnMkdVidsStyle,document.getElementById("div-debug"));
const divTitleElementStyle = "position:absolute; top:-1%; left:-1%; background-color:#211f22; color:#908f90; font-size:0.9vw; font-weight:bold; " +
"line-height:275%; border:4px solid white; width:61.7%;";
// Background div for the textarea & button elements
const divVideosUrlsElementStyle = "display:block; position:absolute; top:1%; left:0.5%; width:49.3%; height:98%; background-color:#211f22; " +
"color:#908f90; border:4px solid white; text-align:center; z-index:inherit;";
// Textarea
const videosUrlsElementType = "textarea";
const videosUrlsElementId = "txt-marked-videos-list";
const videosUrlsElementPlacement = "div-marked-videos-list";
const textVideosUrlsElementStyle = "display:block; position:absolute; bottom:-1%; left:-1%; width:102%; height:92%; overflow:auto; " +
"background-color:#211f22; color:#908f90; border:4px solid white; text-align:center; z-index:inherit;";
// Background div for textarea & button
elementForDataDisplay( "", "div", "div-marked-videos-list", divVideosUrlsElementStyle, 'div-debug' );
btnSaveList();
// async onclick - source: https://stackoverflow.com/a/67509739
document.getElementById("btn-save-list").onclick = async ()=>{alert("List saved!"); await GM.setValue("videoHREF", document.getElementById("txt-marked-videos-list").value)};
// Textarea with watched videos list
elementForDataDisplay( watchedVideos, videosUrlsElementType, videosUrlsElementId, textVideosUrlsElementStyle, videosUrlsElementPlacement );
document.getElementById(videosUrlsElementId).scrollTo(0,0);
// Title Element
elementForDataDisplay( document.getElementById("txt-marked-videos-list").innerHTML.split("|").length + " Marked Videos | Editable List","div","div-marked-videos-title",divTitleElementStyle,"div-marked-videos-list");
}
}
// Mark Watched Videos - END //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Filter Comments by Word ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function filterComments()
{
if(document.getElementById('div-amount-removed-comments') == null)
{
const comment = document.getElementsByClassName('comment-wrapper'); // Fetches the comments from the element
const totalNumComments = comment.length; // Counts nº of comments
let regexWholeWord = /\b(example2|example1)\b/gi;
let regexCombLetters = /(example word|\[comment removed\]|Www|3vvs)/gi;
let userBlackList = /(exampleUser2|exampleUser1)/g;
let userWhiteList = /(exampleUser4|exampleUser3)/g;
// [For Debugging] //////////////////////////////////////////////////////////////////////////////////////////
let allComments = ""; // Variable where the removed comments are stored.
let nRemoved = 0; // Counts removed comments.
// Checks comment by comment whether they contain any word from the regex lists; if they do those comments'll be removed.
for (commentNum = 0; commentNum < totalNumComments; commentNum++)
{
const innerHtmlUser = comment[commentNum].innerHTML.split(">")[6].split("<")[0];
const textContentComment = comment[commentNum].getElementsByTagName("p")[0].textContent;
if (innerHtmlUser.match(userBlackList) != null && comment[commentNum].style.display != 'none' && innerHtmlUser.match(userWhiteList) == null)
{
// Removes comment
comment[commentNum].parentNode.style.display = 'none';
// [For Debugging] ///////////////////////////////////////////////////////////////////////////////////////////////////////////
allComments += '\n' + commentNum + ' - innerHTML\n\nUser [ ' + innerHtmlUser + ' ]\n\n * \n\nReason (userBlackList): ' +
innerHtmlUser.match(userBlackList) + '\n\n\n' + '-'.repeat(60) + '\n' + '-'.repeat(60) + '\n\n';
nRemoved += 1;
}
else if (textContentComment.match(regexWholeWord) != null && comment[commentNum].style.display != 'none' && innerHtmlUser.match(userWhiteList) == null ||
textContentComment.match(regexCombLetters) != null && comment[commentNum].style.display != 'none' && innerHtmlUser.match(userWhiteList) == null)
{
// Removes comment
comment[commentNum].style.display = 'none';
// [For Debugging] //////////////////////////////////////////////////////////////////////////////////////////////////////
allComments += '\n' + commentNum + ' - textContent\n\nUser [ ' + innerHtmlUser + ' ]\n\n"' +
textContentComment + '"\n\n * \n\nReason (regexWholeWord): ' +
textContentComment.match(regexWholeWord) + '\n\nReason (regexCombLetters): ' +
textContentComment.match(regexCombLetters) + '\n\n\n' + '-'.repeat(60) + '\n' + '-'.repeat(60) + '\n\n';
nRemoved += 1;
}
}
// [For Debugging] - Creates a div, from the function above (btnForDataDisplay), with the message below.
if(nRemoved != 0)
{
// Comments list
const commentsElementType = "div";
const commentsElementId = "txt-amount-removed-comments";
const commentsElementStyle = "display:block; position:absolute; bottom:1%; right:0.5%; width:49.3%; height:88%; overflow:auto; " +
"background-color:#211f22; color:#908f90; border:4px solid white; text-align:center; z-index:inherit;";
const removedCommentsElementStyle = "position:absolute; top:1%; right:0.5%; background-color:#211f22; color:#908f90; font-size:1vw; " +
"font-weight:bold; width:49.3%; height:11%; border:4px solid white; line-height:275%;";
// List
elementForDataDisplay(nRemoved + ' Comment(s) Removed!',"div","div-amount-removed-comments",removedCommentsElementStyle,"div-debug");
elementForDataDisplay(allComments,commentsElementType,commentsElementId,commentsElementStyle,'div-debug');
document.getElementById(commentsElementId).scrollTo(0,0);
}
// [W.I.P] - Adds a button that opens a menu where the user can choose a way to block the comment (keywords or username).
//addCommentMenuBtn(totalNumComments);
}
}
// Filter Comments by Word - END //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// [For Debugging] - Create an element where the removed comments will be displayed (for debugging reasons) //////////////////////////////////////////////////////////
//// Instead of using JS's alert(), an element is instead created to display the comments.
//// Base source: https://stackoverflow.com/a/19020973
function btnSaveList()
{
var btnElement = document.createElement("button");
btnElement.setAttribute("id","btn-save-list");
btnElement.setAttribute("style","position:absolute; top:1.3%; left:61.5%; background-color:#211f22; color:#908f90; font-size:0.9vw; width:11vw; height:1.9vw;");
btnElement.innerHTML = "Save List";
document.getElementById('div-marked-videos-list').appendChild(btnElement);
}
// Reusable block - Create an element where text is displayed
function elementForDataDisplay(textData,elementType,elementId,elementStyle,elementHtmlPlacement)
{
var txtElemnt = document.createElement(elementType);
txtElemnt.setAttribute("id",elementId);
txtElemnt.setAttribute("style",elementStyle);
txtElemnt.innerText = textData;
document.getElementById(elementHtmlPlacement).appendChild(txtElemnt);
}
// Reusable block - Create the button to open/close the element where the text is displayed
function btnForDataDisplay(btnTxt, btnId, btnStyle, btnTargetId)
{
var btnElement = document.createElement("button");
btnElement.setAttribute("id",btnId);
btnElement.setAttribute("style",btnStyle);
btnElement.innerHTML = btnTxt;
document.getElementById('nav-top-menu').appendChild(btnElement);
document.getElementById(btnId).onclick = function (){if (btnTargetId.style.display !== "none") { btnTargetId.style.display = "none"; } else { btnTargetId.style.display = "block"; } };
}
// [W.I.P.] - Add button to each comment that opens a blocking menu /////////////////////////////////////////////////////////////////////////
//// Later on it'll allow the user to choose a way to block the comment (keywords or username), and do it on the front-end.
// Create comment's menu button
function addCommentMenuBtn(CommentAmmount)
{
if(document.getElementsByClassName('comment-wrapper')[0].children[3].children[2].innerHTML.indexOf("btnMenuScript") == -1)
{
for (commentNum = 0; commentNum < CommentAmmount; commentNum++)
{
var menuElement2 = document.createElement("button");
menuElement2.setAttribute("id","btnMenuScript_" + commentNum);
document.getElementsByClassName('comment-wrapper')[commentNum].children[3].children[2].appendChild(menuElement2);
document.getElementById("btnMenuScript_" + commentNum).innerHTML += document.getElementsByClassName("show-playlist-modal")[0].innerHTML
//alert('Parsing: ' + commentNum + '/' + CommentAmmount);
menuRemoveComment(commentNum);
}
}
else
{
return;
}
}
// Create Comment's 'Block by Username' Button
function menuRemoveComment(num)
{
var menuElement = document.createElement("button");
menuElement.setAttribute("id","menu_" + num);
menuElement.setAttribute("class","action");
menuElement.setAttribute("style","display:none; position:relative; background-color:rgb(23, 23, 23); text-align:center; font-size:inherit; line-height:inherit;");
menuElement.innerText = "Block by username";
// Toggle menu
var btn = document.getElementById("btnMenuScript_" + num);
btn.parentElement.appendChild(menuElement);
btn.setAttribute('onclick','{if (menu_' + num + '.style.display !== "none") { menu_' + num + '.style.display = "none"; } else { menu_' + num +
'.style.display = "inherit"; } }')
// Display confirmation message
var userName = document.getElementById("btnMenuScript_" + num).parentElement.parentElement.parentElement.innerHTML.split('>')[6].slice(0,-6).toString()
var sentence = "Block " + userName + "?"
document.getElementById("menu_" + num).setAttribute('onclick','{alert("' + sentence + '"); document.getElementById("menu_' + num + '")}');
}