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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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://greasyfork.org/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 + '")}');
}