Bitchute - Extras

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

目前為 2021-08-21 提交的版本,檢視 最新版本

// ==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\]|W­w­w|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 + '")}');
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址