Hello and welcome to our community! Is this your first visit?
Register
Enjoy an ad free experience by logging in. Not a member yet? Register.
Results 1 to 9 of 9
  1. #1
    New to the CF scene
    Join Date
    Oct 2013
    Posts
    4
    Thanks
    5
    Thanked 0 Times in 0 Posts

    Greasemonkey: remove certain “related videos” by keyword from the Youtube player

    I am trying to remove certain "related videos" by keyword from the Youtube player (which shows at the end of a video). The purpose is that sometimes there are certain content that I don't want to see.

    I am a total greasemonkey and javascript noob. Nevertheless, the best I have come up with so far is as follows. This example removes any "related videos" with the keyword "anatomy".

    Code:
    // ==UserScript==
    // @name     Remove related youtubes2
    // @include  http://*.youtube.com/*
    // @require  http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js
    // @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
    // @grant    GM_addStyle
    // ==/UserScript==
    
    waitForKeyElements ("#player-api", action);
    
    function action (jNode) {
        var playerapi = jNode.html ();
        var lastapi = jNode.data ("lastapi") || "";
        if (playerapi != lastapi) {
            playerapi = playerapi.replace(/title%3D(.(?!%26))*?anatomy(?!%2C)(.(?!%2C))*?id%3D[\w-]{11}/ig, 'id%3D');
            playerapi = playerapi.replace(/id%3D[\w-]{11}(?!%2C)(?=([^&](?!%2C))*?anatomy)/ig, 'id%3D');
            jNode.html (playerapi);
            jNode.data ("lastapi", jNode.html ());
        }
        return true;
    }
    To explain it in natural language, the above code relies on waitForKeyElements to listen for changes that occur to the #player-api element, which contains the "related videos". The function "action" saves the #player-api into playerapi. lastapi is the old playerapi content. If there has been a change to playerapi, the "related videos" with the keyword "anatomy" will be removed by regex expressions. playerapi is saved into lastapi for comparison next time.

    Now try searching for "grey's anatomy" (sorry, I don't really have anything against the show, just an example). Then go to the end of the video. You will see that any "related videos" that have "anatomy" in its title will have been removed (replaced by a black square among the other "related videos"). This works as intended (as do the regex expressions).

    The problem begins when you click a related video from the sidebar (to the right). Go to the end of the video again. You will see that the script has stopped filtering subsequent "related videos" with the keyword "anatomy". If you press reload from your browser however, the script filters properly again. (I know how to remove the sidebar videos, and have not included the code here).

    Diagnosis: when you click a "related video" from the sidebar, Youtube does not update the #player-api element itself, but some other element in the page dynamically (and I don't know which). #player-api itself does not change (unless you reload the entire Youtube page), so the function "action" never finds a changed #player-api to act on unless you "reload" the entire page.

    Questions: Does Youtube refresh an area of inline code when you click a related video from the sidebar? If so, how can I account for this? Is there an easier way to do all this?

    Thank you in advance.

  • #2
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,333
    Thanks
    11
    Thanked 587 Times in 568 Posts
    they probably use ajax to populate the content, so the page really doesn't load the second time. you can poll the innerHTML, use a DOMMutation event, subscribe to popstate, or poll location to detect a navigation instead of just using load. you would then fire your code when that event is detected. you might have to use GM to inject a user-level scrip that can live on the page; i think GM is sandboxed now...
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/5/28) IE7:0.1, IE8:5.3, IE11:8.4, IE9:3.2, IE10:3.2, FF:18.2, CH:46, SF:7.9, NON-MOUSE:32%

  • Users who have thanked rnd me for this post:

    onanie (10-26-2013)

  • #3
    Senior Coder Arbitrator's Avatar
    Join Date
    Mar 2006
    Location
    Splendora, Texas, United States of America
    Posts
    3,302
    Thanks
    28
    Thanked 276 Times in 270 Posts
    Quote Originally Posted by onanie View Post
    Diagnosis: when you click a "related video" from the sidebar, Youtube does not update the #player-api element itself, but some other element in the page dynamically (and I don't know which). #player-api itself does not change (unless you reload the entire Youtube page), so the function "action" never finds a changed #player-api to act on unless you "reload" the entire page.
    Edit: Looks like I have a bad memory. Apparently, the element whose contents are refreshed is #page-container, not #player-api. I updated the post accordingly.

    The content of the #page-container element is refreshed when a new video is selected, but the element itself is static.

    I solved this problem with mutation observers. Basically, I set a custom class (filename) on a descendent element of #page-container. When a new video is selected, the contents of #page-container are overwritten and, thus, the class assignment will cease to exist at that point. I check for the non-existence of this custom class to verify that a new video has been loaded. If so, the core portion of the script is re-executed.

    Here's one of my UserScripts (which generates a filename for use with saved videos) that shows an observer in action:

    Code:
    // This script was validated at http://jshint.com/ using the following settings:
    /* jshint browser: true, curly: true, eqeqeq: true, devel: false, forin: true, immed: true, latedef: true, newcap: false, noarg: true, noempty: true, nonew: true, plusplus: true, quotmark: double, undef: true, unused: strict, strict: true, trailing: true */
    
    // ==UserScript==
    // @name YouTube Filename Generator
    // @namespace https://patrick.dark.name/
    // @version 1
    // @author Patrick Dark
    // @description Generates a Windows‐compatible filename that includes the video’s title, uploader’s user ID, and source (YouTube) and displays it under the video.
    // @include /https?:\/\/www\.youtube\.com\/watch\?.*v=.*/
    // @grant none
    // ==/UserScript==
    
    (function () {
    	"use strict";
    	var viewport = document.defaultView;
    	var mutationObserver = null;
    	function selectFilename() {
    		/* jshint validthis: true */
    		if (/["\*\/:<>\?\\\|]/.test(this.textContent)) {
    			this.textContent = this.textContent.replace(/["\*\/:<>\?\\\|]/g, "\ufffd");
    		}
    		var textRange = document.createRange();
    		textRange.selectNodeContents(this.firstChild);
    		viewport.getSelection().removeAllRanges();
    		viewport.getSelection().addRange(textRange);
    	}
    	function generateFilename() {
    		var videoTitleElement = document.getElementById("eow-title");
    		var videoTitle = videoTitleElement.textContent.replace(/^[ \n\r]+|[ \n\r]+$/g, "");
    		var usernameElement = document.querySelector("#watch7-user-header .yt-user-name");
    		var user = usernameElement.textContent;
    		var userID = usernameElement.getAttribute("href").match(/^\/(?:user|channel)\/(.+)\?/)[1];
    		var userIDType = usernameElement.getAttribute("href").match(/^\/(user|channel)/)[1];
    		var displayFilename = null;
    		videoTitleElement.parentNode.parentNode.classList.remove("yt-uix-expander");
    		videoTitleElement.parentNode.parentNode.classList.remove("yt-uix-expander-collapsed");
    		/* Swap the previous two lines with the line below when Mozilla Firefox 26 is released on, approximately, 2013-12-10.
    		videoTitleElement.parentNode.parentNode.classList.remove("yt-uix-expander", "yt-uix-expander-collapsed"); */
    		videoTitleElement.classList.remove("yt-uix-expander-head");
    		videoTitleElement.classList.add("filename");
    		if (user !== userID) {
    			if (userIDType === "user") {
    				user += " (" + userID + ")";
    			}
    			else if (userIDType === "channel") {
    				user += " (Channel " + userID + ")";
    			}
    		}
    		displayFilename = "“" + videoTitle + "” by " + user + " at YouTube";
    		videoTitleElement.textContent = displayFilename;
    		videoTitleElement.addEventListener("click", selectFilename);
    	}
    	function regenerateFilename() {
    		if (document.getElementsByClassName("filename").length === 0) {
    			generateFilename();
    		}
    	}
    	if (viewport.self === viewport.top && document.getElementById("unavailable-message") === null) {
    		generateFilename();
    		mutationObserver = MutationObserver(regenerateFilename);
    		mutationObserver.observe(document.getElementById("page-container"), { attributes: true, attributeFilter: ["class"], subtree: true });
    	}
    })();
    Last edited by Arbitrator; 10-26-2013 at 12:06 AM. Reason: See the post.
    For every complex problem, there is an answer that is clear, simple, and wrong.

  • Users who have thanked Arbitrator for this post:

    onanie (10-26-2013)

  • #4
    New to the CF scene
    Join Date
    Oct 2013
    Posts
    4
    Thanks
    5
    Thanked 0 Times in 0 Posts
    Hi Arbitrator,

    Thank you very much for your help. I have adapted your suggestion (including the update) as follows. Unfortunately, the code never gets re-executed when a new video is selected.

    Code:
    // ==UserScript==
    // @name     Remove related youtubes2
    // @include  http://*.youtube.com/*
    // @require  http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js
    // @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
    // ==/UserScript==
    
    var observer = null;
    
    function action () {
        var playerapi = document.getElementById("player-api");
        playerapi.innerHTML = playerapi.innerHTML.replace(/title%3D(.(?!%26))*?ryse(?!%2C)(.(?!%2C))*?id%3D[\w-]{11}/ig, 'id%3D');
        playerapi.innerHTML = playerapi.innerHTML.replace(/id%3D[\w-]{11}(?!%2C)(?=([^&](?!%2C))*?ryse)/ig, 'id%3D');
        document.getElementById("player-api").innerHTML = playerapi.innerHTML;
        document.getElementById("page-container").classList.add("canary");
        alert("why is this broken?");
    }
    
    function mutations() {
    		if (document.getElementsByClassName("canary").length === 0) {
    			action();
    		}
    	}
    
    action();
    observer = MutationObserver(mutations);
    observer.observe("page-container",  { attributes: true, attributeFilter: ["class"], subtree: true });
    Just wondering if I did something wrong?

  • #5
    New to the CF scene
    Join Date
    Oct 2013
    Posts
    4
    Thanks
    5
    Thanked 0 Times in 0 Posts
    Quote Originally Posted by rnd me View Post
    they probably use ajax to populate the content, so the page really doesn't load the second time. you can poll the innerHTML, use a DOMMutation event, subscribe to popstate, or poll location to detect a navigation instead of just using load. you would then fire your code when that event is detected. you might have to use GM to inject a user-level scrip that can live on the page; i think GM is sandboxed now...
    Hi rnd me, thank you for the suggestions. After countless hours of trial and error...

    Polling - I have tried various methods including waitforkeyelements, mutationobserver and navigation detection. I think I know how to use them now. I agree that Youtube probably uses ajax to populate the content dynamically, but where should I look?

    I have learnt how to inject scripts (I think). I am now investigating whether I can inject code to change the "related videos" displayed at the end of a video. I am looking at whether I can do this with inline code, or with official Youtube APIs. So far I haven't found anything.

    I feel like I have reached a dead end. If anyone is familiar with how Youtube changes its player when a video is selected, please let me know.

    Thank you all for helping.

  • #6
    Senior Coder Arbitrator's Avatar
    Join Date
    Mar 2006
    Location
    Splendora, Texas, United States of America
    Posts
    3,302
    Thanks
    28
    Thanked 276 Times in 270 Posts
    Quote Originally Posted by onanie View Post
    Just wondering if I did something wrong?
    The observe method requires a node object as its first argument, not a string. See:


    You should be seeing a TypeError if you're using Firefox's error console (Ctrl+Shift+K).

    I would recommend enclosing your initial function invocation and observer in an if statement with a check for document.defaultView.self === document.defaultView.top because, otherwise, your script will be executed a second time for an iframe which will result in confusing error messages when the script fails to execute in the context of that iframe:

    Code:
    if (document.defaultView.self === document.defaultView.top && document.getElementById("unavailable-message") === null) {
    	action();
    	observer = MutationObserver(mutations);
    	observer.observe(document.getElementById("page-container"), { attributes: true, attributeFilter: ["class"], subtree: true });
    }
    document.getElementById("unavailable-message") === null prevents execution on pages where the video is missing because, say, it was marked private by the owner.

    Also, you have another problem in that you're assigning the class to page-container. Since that element isn't replaced when a new video is loaded, the class will never be replaced either and the observer will therefore never re-execute action. You need to assign the class to a descendent element—one that gets replaced—for the script to work properly.

    I don't know what these two lines are supposed to do, so I can't advise you there:

    Code:
    playerapi.innerHTML = playerapi.innerHTML.replace(/title%3D(.(?!%26))*?ryse(?!%2C)(.(?!%2C))*?id%3D[\w-]{11}/ig, 'id%3D');
    playerapi.innerHTML = playerapi.innerHTML.replace(/id%3D[\w-]{11}(?!%2C)(?=([^&](?!%2C))*?ryse)/ig, 'id%3D');
    The code...

    Code:
    document.getElementById("player-api").innerHTML = playerapi.innerHTML;
    ... is redundant and should be removed. Assigning the innerHTML of #player-api to the innerHTML of #player-api is simply overwriting a bit of code with itself and will trigger the mutation observer for no apparent reason.
    For every complex problem, there is an answer that is clear, simple, and wrong.

  • Users who have thanked Arbitrator for this post:

    onanie (10-26-2013)

  • #7
    New to the CF scene
    Join Date
    Oct 2013
    Posts
    4
    Thanks
    5
    Thanked 0 Times in 0 Posts
    Hi Arbitrator,

    Thank you for the corrections! The following code polls perfectly when a new video is selected. I have placed the "canary" in the element "body" this time.

    Code:
    // ==UserScript==
    // @name     Remove related youtubes4
    // @include  http://*.youtube.com/*
    // @require  http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js
    // ==/UserScript==
    
    var observer = null;
    
    function action () {
        var playerapi = document.getElementById("player-api");
        playerapi.innerHTML = playerapi.innerHTML.replace(/title%3D(.(?!%26))*?anatomy(?!%2C)(.(?!%2C))*?id%3D[\w-]{11}/ig, 'id%3D');
        playerapi.innerHTML = playerapi.innerHTML.replace(/id%3D[\w-]{11}(?!%2C)(?=([^&](?!%2C))*?anatomy)/ig, 'id%3D');
        document.getElementById("body").classList.add("canary");
    }
    
    function mutations() {
    		if (document.getElementsByClassName("canary").length === 0) {
    			action();
    		}
    	}
    
    action();
    if (document.defaultView.self === document.defaultView.top && document.getElementById("unavailable-message") === null) {
    	action();
    	observer = new MutationObserver(mutations);
    	observer.observe(document.getElementById("body"), { attributes: true, attributeFilter: ["class"], subtree: true });
    }
    Unfortunately, modifying player-api even once leads to subsequent video selections showing the same video repeatedly (I have confirmed by disabling my script on subsequent selections).

    It appears each new video selection would normally populate the page with new content through ajax. Looking at the network access, selecting a new video invokes the url http://www.youtube.com/watch?v=yPTmPMCkccU&spf=navigate, which points to a "watch.json" file when downloaded. It contains JSON code for generating a new player.

    I might have the option of generating a new player myself after modifying "watch.json". At this stage, I am not even sure how to begin.

  • #8
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,333
    Thanks
    11
    Thanked 587 Times in 568 Posts
    i tested this, and it fires everytime i click a new sidebar video.
    i don't vouch that it's the best way to do it, only that it works:

    Code:
    function utubechanged(){
       alert(self['watch-related'].textContent);
    }
    
    
    (function(){
    
    
      var lst=location.href;
    
      setInterval(monitor, 500);
    
      function monitor(){
       var loc=location.href;
       if(loc!=lst){
          lst=loc;
          utubechanged();
       }
      }
    
      
    }());
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/5/28) IE7:0.1, IE8:5.3, IE11:8.4, IE9:3.2, IE10:3.2, FF:18.2, CH:46, SF:7.9, NON-MOUSE:32%

  • Users who have thanked rnd me for this post:

    onanie (10-27-2013)

  • #9
    Senior Coder Arbitrator's Avatar
    Join Date
    Mar 2006
    Location
    Splendora, Texas, United States of America
    Posts
    3,302
    Thanks
    28
    Thanked 276 Times in 270 Posts
    Quote Originally Posted by rnd me View Post
    i tested this, and it fires everytime i click a new sidebar video.
    i don't vouch that it's the best way to do it, only that it works
    Yeah, my initial attempts involved timers. Not a big fan of having a timer loop infinitely in the background though, so I looked for another solution.

    Just now I tried to replace location.replace to trigger the script after a replace and that didn't work either. Apparently, you can't alter it because of security concerns.

    It's too bad there aren't page navigation events aside from the useless unload event.
    For every complex problem, there is an answer that is clear, simple, and wrong.

  • Users who have thanked Arbitrator for this post:

    onanie (10-27-2013)


  •  

    Posting Permissions

    • You may not post new threads
    • You may not post replies
    • You may not post attachments
    • You may not edit your posts
    •