YePpHa / YouTubeCenter

YouTube Center is a userscript designed to expand the functionality of YouTube. It includes the ability to download the video you're watching, auto selecting your preferred video quality and much more.
MIT License
2.89k stars 520 forks source link

Feature Request: Video Counter #1060

Open Xylemon opened 9 years ago

Xylemon commented 9 years ago

An option to show the number of videos next to the user name would be nice to have again.

Yonezpt commented 9 years ago

For me that feature wasn't the most useful by itself -just showing the amount of uploaded videos- instead it was the fact that clicking on it linked you directly to the video tab of that user. That was really useful, today we have to click on the username, wait for the topbar to get in place otherwise we will misclick and then we can click on the video tab.

If you include this video counter feature, YePpHa, also make it link to the user's video tab.

Eisys commented 9 years ago

Yes, please add this! I miss this feature almost daily.

Yonezpt commented 9 years ago

Made up a quick concept for this feature based on the old layout, the "verified" symbol is removed and the username text will act as the verified signal instead and change to the blue color (same blue as the like bar). Non-verified users/channels will keep the standard aspect. The interpunct is between two spaces and these are all inside a span element while the videos' count link is inside an "a" element so that the underline wouldn't show beneath the interpunct. Clicking on the videos link will lead to the channel/user video page.

Font-size used in the old youtube layout was 11px and I believe the color was the standard "gray" found in the CSS options.

Maybe this might be useful in some way so I share this just in case.

untitled-1

Now the only thing left is to find an effective way to return the number of videos uploaded by the user. The hovercards have that information, maybe that might be a good point to start with.

evelynharthbrooke commented 9 years ago

@Yonezpt Great concept, and all you gotta do now is make it redirect to the Channel's video page when you click on the button. By the way, can't you implement it yourself instead of waiting for YePpHa to implement it? You are a collaborator after all. You can push directly to the repo instead of submitting a pull request. :smiley:

Yonezpt commented 9 years ago

@KamranMackey I have already built a working concept which I am currently using, but I wouldn't be comfortable with pushing the code myself even if YePpHa allowed me. I only have about 2months-ish of javascript knowledge that I have been acquiring during my free-time during the past couple of months, I am still an amateur at this and I don't want to end up creating more trouble which will cause YePpHa to work twice: one to fix whatever I broke, two to implement the feature correctly.

Also he codes in a way that confuses me, so I would almost certainly break something by mistake.

evelynharthbrooke commented 9 years ago

@Yonezpt Have you thought about studying his code? That will help you figure out how to implement things into YouTube Center. Or you could use Codecademy and take it's JavaScript course. It's free to take it. Also it's a completely online course.

Yonezpt commented 9 years ago

I have tried, but this is just a hobby for me, I don't have interest on getting into it that much, at least not for now.

evelynharthbrooke commented 9 years ago

@Yonezpt Yeah, but you could at least try and implement the concept into your fork of YouTube Center.

On Fri, Nov 7, 2014 at 7:39 PM, Particle notifications@github.com wrote:

I have tried, but this is just a hobby for me, I don't have interest on getting into it that much, at least not for now.

Reply to this email directly or view it on GitHub https://github.com/YePpHa/YouTubeCenter/issues/1060#issuecomment-62242309 .

Yonezpt commented 9 years ago

I have been trying for the past couple of days, there's always something that breaks. Either the options go bananas, or the eventlisteners drop dead, or the player elements get twisted, or etc.. His code is clean and reasonably optimized, but I just don't understand it very well. Everytime I think I got it something goes wrong.

I can, however, create a simple userscript to be used with greasemonkey or tampermonkey if anyone is interested in it. This isn't something that will be supported, there's no place for doing it and I will not be using YePpHa's space for it, so the code is shared as is and if it works then it works, if not then too bad. It works with the current Firefox version 33.0.3 and Chrome 38.0.2125.111, at least.

// ==UserScript==
// @version     1
// @name        Youtube user video counter
// @namespace   Namespace
// @domain      youtube.com
// @include     https://www.youtube.com*
// @include     http://www.youtube.com*
// @run-at      document-start
// @grant       none
// ==/UserScript==

function injector(a){
  var injection = document.createElement('script');
  injection.innerHTML = a;
  document.head.appendChild(injection);
}
var code = function(a){
  // We will store the number of videos for the users which we have already fetched the video count for
  // This will save bandwidth for the user and youtube, and it will only last until the page refreshes/closes
  var store = {};
  function username(b){
    // We check if we are in a page where we want to display the user videos, otherwise we don't need to do anything
    // We also don't want to run the function again if the video count is already existent
    if(/\/watch/.test(location.href) && !document.getElementById('videocount')){
      // We will declare variables, create elements and fetch the user info in the current page if available as well as the 'verified' symbol
      var videos,
          link = document.createElement('a'),
          div = document.createElement('div'),
          span = document.createElement('span'),
          user = document.querySelector('.yt-user-info>a'),
          verified = document.querySelector('[aria-label="Verified"]');
      // Set the static data before fetching the video count for a more organized code
      link.id = 'videocount';
      link.href = user.getAttribute('href')+'/videos';
      link.setAttribute('style','font-size: 11px; color: #666; display: initial; font-weight: initial; overflow: initial; vertical-align: initial');
      span.setAttribute('style','font-size: 11px; color: #666;');
      span.innerHTML = ' · ';
      document.querySelector('.yt-user-info').appendChild(span);
      document.querySelector('.yt-user-info').appendChild(link);
      // Let's check if the user has the verified symbol so we can make things more pretty by turning his name blue
      if(verified){
        verified.parentNode.removeChild(verified);
        user.className = user.className + ' yt-uix-tooltip';
        user.setAttribute('data-tooltip-text','Verified');
        user.style.color = '#167ac6';
      }
      // Check if we already fetched the video count for this user and use the already existing count if we do
      // or fetch the video count instead if we don't have that data already
      if(store[user.getAttribute('data-ytid')]){
        link.textContent = store[user.getAttribute('data-ytid')];
      }else{
        var request = new XMLHttpRequest();
        request.open("GET", '/playlist?list=' + user.getAttribute('data-ytid').replace('UC','UU') + '&spf=navigate');
        request.onreadystatechange = function(){
          if(4 == request.readyState){
            videos = JSON.parse(request.responseText);
            div.innerHTML = videos.body.content;
            link.className = 'spf-link';
            link.textContent = div.querySelectorAll('ul.pl-header-details li')[1].textContent;
            store[user.getAttribute('data-ytid')] = link.textContent;
          }
        };
        request.send();
      }
    }
  }
  // Runs the function when the page is loading and has finished loading
  window.addEventListener('readystatechange',username,true);
  // Runs the function when the page changes via SPF method: https://github.com/youtube/spfjs/
  window.addEventListener('spfdone',username);
};
// Let's not run this in iframes
if(top === self)injector('('+code+')()');

Clicking on the number of videos link will lead you to the user's video tab. You can install this with greasemonkey for firefox or tampermonkey for chrome. Hope you enjoy it, at least until YePpHa makes his superior version natively in YTC.

evelynharthbrooke commented 9 years ago

Looks great. Don't know why it wouldn't work in YouTube Center though, but now we wait till Jeppe (@YePpHa) implements it into YouTube Center. On Nov 9, 2014 8:05 PM, "Particle" notifications@github.com wrote:

I have been trying for the past couple of days, there's always something that breaks. Either the options go bananas, or the eventlisteners drop dead, or the player elements get twisted, or etc.. His code is clean and reasonably optimized, but I just don't understand it very well. Everytime I think I got it something goes wrong.

I can, however, create a simple userscript to be used with greasemonkey or tampermonkey if anyone is interested in it. This isn't something that will be supported, there's no place for doing it and I will not be using YePpHa's space for it, so the code is shared as is and if it works then it works, if not then too bad. It works with the current Firefox version 33.0.3 and Chrome 38.0.2125.111, at least.

// ==UserScript==// @version 1// @name Youtube user video counter// @namespace Namespace// @domain youtube.com// @include https://www.youtube.com// @include http://www.youtube.com// @run-at document-start// @grant none// ==/UserScript== function injector(a){ var injection = document.createElement('script'); injection.innerHTML = a; document.head.appendChild(injection); }var code = function(a){ // We will store the number of videos for the users which we have already fetched the video count for // This will save bandwidth for the user and youtube, and it will only last until the page refreshes/closes var store = {}; function username(b){ // We check if we are in a page where we want to display the user videos, otherwise we don't need to do anything // We also don't want to run the function again if the video count is already existent if(/\/watch/.test(location.href) && !document.getElementById('videocount')){ // We will declare variables, create elements and fetch the user info in the current page if available as well as the 'verified' symbol var videos, link = document.createElement('a'), div = document.createElement('div'), span = document.createElement('span'), user = document.querySelector('.yt-user-info .yt-user-name'), verified = document.querySelector('[aria-label="Verified"]'); // Set the static data before fetching the video count for a more organized code link.id = 'videocount'; link.href = user.getAttribute('href')+'/videos'; link.setAttribute('style','font-size: 11px; color: #666; display: initial; font-weight: initial; overflow: initial; vertical-align: initial'); span.setAttribute('style','font-size: 11px; color: #666;'); span.innerHTML = ' · '; document.querySelector('.yt-user-info').appendChild(span); document.querySelector('.yt-user-info').appendChild(link); // Let's check if the user has the verified symbol so we can make things more pretty by turning his name blue if(verified){ verified.parentNode.removeChild(verified); user.className = user.className + ' yt-uix-tooltip'; user.setAttribute('data-tooltip-text','Verified'); user.style.color = '#167ac6'; } // Check if we already fetched the video count for this user and use the already existing count if we do // or fetch the video count instead if we don't have that data already if(store[user.getAttribute('data-ytid')]){ link.textContent = store[user.getAttribute('data-ytid')]; }else{ var request = new XMLHttpRequest(); request.open("GET", '/playlist?list=' + user.getAttribute('data-ytid').replace('UC','UU') + '&spf=navigate'); request.onreadystatechange = function(){ if(4 == request.readyState){ videos = JSON.parse(request.responseText); div.innerHTML = videos.body.content; link.className = 'spf-link'; link.textContent = div.querySelectorAll('ul.pl-header-details li')[1].textContent; store[user.getAttribute('data-ytid')] = link.textContent; } }; request.send(); } } } // Runs the function when the page is loading and has finished loading window.addEventListener('readystatechange',username,true); // Runs the function when the page changes via SPF method: https://github.com/youtube/spfjs/ window.addEventListener('spfdone',username); };// Let's not run this in iframesif(top === self)injector('('+code+')(window)');

Clicking on the number of videos link will lead you to the user's video tab. You can install this with greasemonkey for firefox or tampermonkey for chrome. Hope you enjoy it, at least until YePpHa makes his superior version natively in YTC.

Reply to this email directly or view it on GitHub https://github.com/YePpHa/YouTubeCenter/issues/1060#issuecomment-62335542 .

YePpHa commented 9 years ago

@Yonezpt just to clarify some things: I don't think my code is clean and reasonable optimized myself. The reason for this is that I change my coding style over time and YouTube Center is coded over a couple of years and this means that I have quite a few different way to go about things. If you look at the beginning of the YouTube Center code, inside the main function, you will be able to find functions starting with a $, which I now think is quite a ridiculous thing to do and I have no clue why I actually did it. Another thing is that I might even have duplicate functions because it's quite hard to keep track of what utility functions I've already implemented and I then keep adding them because I named the function something I would not be able to find with a search function.

@Yonezpt if you do make something and you want it inside YouTube Center just make a pull request (you might actually not be able to because you're a collaborator). I'm totally fine with you taking up some of my time.

Also what I got from this conversation is that not many knows how YouTube Center is structured. This will be really short, but will probably help people in the future if they want to contribute to YouTube Center. Anyway here goes.

The way YouTube Center works is that it has it main scope, which is wrapped inside (function(){})() as any person knowledgeable about JavaScript will do (and then export functions into the window scope). Inside the brackets YouTube Center handles everything relating to extension support and userscript support; giving YouTube Center access to cross origin xhr, better settings storing methods and etc. It's also there it make a pipeline between the page and the secluded userscript scope. This pipeline is used to send actions from the page to the userscript scope to perform certain actions like xhr. A few years back, the pipeline were not required as the userscript scope and the window scope had full access to each other, but recently browser security have changed this. Another important part of this first step is the injection of the main part of YouTube Center, which is all contained inside the main_function function.

Inside the main_function function it keeps the utils functions at the very top and then it defines the ytcenter object where it then initializes modules (I should at some time in the future split this from the main file and make my build script merge them automatically). These modules are i.e. the resize feature, thumbnail, page loading (execute code depending on specific events), and etc. I should note that the modules have their own scope, which YouTube Center's scope will not be able to access without the modules exporting their functions. In some cases I might not make a module and I just make them be included in the main scope, which should be avoided at all cost, but I don't always follow my own rules, unfortunately. An example of this is the download feature in YouTube Center. At the end there is the code that executes the modules and initializes everything. This is used in conjunction with the page load module. It's also there the player listeners are attached.

This was my short version of how YouTube Center is structured and I could probably write many pages on how YouTube Center works to the smallest detail.

Anyway, I'm good at going off topic, but back to the topic. I really like your concept I can try to implement it at some time and I my development time could potentially be halved by being able to look at your code which already has the needed elements referenced.

Yonezpt commented 9 years ago

@YePpHa So that's why you had unsafeWindow references mixed with content that runs directly in the page DOM, thanks for the explanation, it made lots of things more clear now. Even with the expected lint gathered over... what, 4 years or so? the code is understandable for the most part, what was continuously throwing me off was the functions that were running inside the greasemonkey sandbox (think that is the correct term) and the ones running outside.

I also am glad that you find my little script possibly useful and I have since then polished it a bit more. It's the same thing as before, but this time it will account for xhr failure. This means that if for some reason the video count xhr fetching fails then the username will remain unchanged as it should since we don't have any data to insert. Another "fix" was to gather the "verified" element data independent of the text it contained because the word "verified" will be different for people that use a different youtube language.

// ==UserScript==
// @version     2
// @name        Youtube user video counter
// @namespace   Namespace
// @domain      youtube.com
// @include     https://www.youtube.com*
// @include     http://www.youtube.com*
// @run-at      document-start
// @grant       none
// ==/UserScript==

function injector(a) {
    var injection = document.createElement('script');
    injection.innerHTML = a;
    document.head.appendChild(injection);
}
var code = function(a) {
    var store = {};

    function username() {
        var videos,
            link,
            div,
            span,
            user,
            verified;

        function videoCounter() {
            link.href = user.getAttribute('href') + '/videos';
            link.setAttribute('style', 'font-size:11px;color:#666;display:initial;font-weight:initial;overflow:initial;vertical-align:initial');
            span.textContent = ' · ';
            span.setAttribute('style', 'font-size:11px;color:#666');
            document.getElementsByClassName('yt-user-info')[0].insertBefore(span, document.getElementById('uploaded-videos'));
            if (verified) {
                user.className += ' yt-uix-tooltip';
                user.setAttribute('data-tooltip-text', verified.getAttribute('data-tooltip-text'));
                user.style.color = '#167ac6';
                verified.remove();
            }
            div = user = span = link = videos = verified = null;
        }
        if (/\/watch/.test(location.href) && !document.getElementById('uploaded-videos')) {
            link = document.createElement('a');
            div = document.createElement('div');
            span = document.createElement('span');
            user = document.querySelector('.yt-user-info>a');
            verified = document.getElementsByClassName('yt-channel-title-icon-verified')[0];
            link.id = 'uploaded-videos';
            document.getElementsByClassName('yt-user-info')[0].appendChild(link);
            if (store[user.getAttribute('data-ytid')]) {
                link.textContent = store[user.getAttribute('data-ytid')];
                videoCounter();
            } else {

                var request = new XMLHttpRequest();
                request.open("GET", '/playlist?list=' + user.getAttribute('data-ytid').replace('UC', 'UU') + '&spf=navigate');
                request.onreadystatechange = function() {
                    if (4 == request.readyState) {
                        videos = JSON.parse(request.responseText);
                        div.innerHTML = videos.body.content;
                        link.className = 'spf-link';
                        link.textContent = div.querySelectorAll('ul.pl-header-details li')[1].textContent;
                        store[user.getAttribute('data-ytid')] = link.textContent;
                        videoCounter();

                    }
                };
                request.send();
            }
        }
    }
    window.addEventListener('readystatechange', username, true);
    window.addEventListener('spfdone', username);
};
if (window.top === window.self) injector('(' + code + ')()');
YePpHa commented 9 years ago

It's definitely a better idea to use the already existing 'verify' string by YouTube. Another thing I noticed is that you're injecting the script into the page, which isn't necessary in this case as you don't access the unsafeWindow. You could, therefore, also use GM_xmlhttprequest and then use the default XMLHttpRequest. Another thing you should do is to wrap your whole code inside a wrapper:

(function(){
  // .. your code
})();

When you create a wrapper you create a local scope for your own script, but this is actually not necessary anymore due to options in both GM and TM that will do this automatically if it hasn't been done. Anyway, it's a good practice to do.

If you're using an injector method you should account for the head element not being available. Normally when you inject a script into the page you will first try to inject it into the body, then the head and if none of them are available then inject it into document.documentElement. This will make the method more reliable as it's actually possible to run a script without the DOM having been loaded yet and that includes document.head.

The final thing is that the event readystatechange is only fired when the readyState of the document changes. So if the page has already been loaded completely it wont run your script due to the event not being fired. You can do this by simply checking document.readyState if it's equals to interactive or complete and then manually run username() without adding the event listener. If it's not interactive or complete then add the event listener:

function username() {
  if (document.readyState !== "interactive" && document.readyState !== "complete") return;
 // ...
}
// ...
if (document.readyState !== "interactive" && document.readyState !== "complete") {
  window.addEventListener('readystatechange', username, true);
} else {
  username();
}
Yonezpt commented 9 years ago

It is? But whenever a user is not using english language, "verify" will not be the same string so the script will fail to fetch the element and its data.

I have been using the standard in-page xhr since the link that it uses to fetch information is within the same domain, so there's no chance of hitting the CORS limitation and also this way it won't depend on greasemonkey's xml (which I suspect that it might add an extra overhead, even if a small one).

Haven't been wrapping it inside the userscript because, as you said, they already do this automatically (fortunately for me). Guess the lazyness beats the good practice for me sometimes.

Regarding the injector method, since I make use of @run-at document-start I assumed it injects the code whenever the first head tag is created, which has been the case ever since I started messing with my hacky scripts. Thought the fallbacks would only be needed if the script is being injected through a custom addon instead of GM/TM. This ties along with the document state since it will be listening right from the begining of the document creation, which even captures the loading, interactive and complete. Again, that's because so far the scripts have been injecting as soon as the head tag is created for me, haven't faced yet an issue where they are injected with a delay big enough to cause conflicts. In fact, I haven't seen them being injected any later than right after head is created, might be because my scripts are still a bit small to cause a considerable injection delay.

But I will save your lessons because I want to memorize this for future coding.

YePpHa commented 9 years ago

You currently retrieve the 'verify' string from YouTube by getting the data-tooltip-text, which is the correct method.

It's fine to use the standard xhr method, but it might just be easier to use GM API. Anyway the current xhr method you use is fine for your current use.

For document-start it will actually run it when it starts to load the DOM and that's at <html> and not <head> so it's possible that head will not be available (or that's what I got from https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness). However this is very dependent on the timing and it's very hard for a script to be run before head is actually injected. It's very dependent on timing, but as initializing the head element is done almost instantly it will probably not break. At document-start it will run when document.readyState === 'loading', which is when the DOM tree is loading (there is a uninitialized state before loading, but it's not really important).

The very simple userscript below will say that document.head is available as even that can't run before head is initialized, but because there's a chance that it can happen it would be ideal that some kind of check is made anyway. Though it's completely up to yourself if you want to do it as the check does have a performance impact, though microscopical little.

// ==UserScript==
// @name            Testing if head is available
// @namespace       I'm from Mars
// @version         0.1
// @description     Not useful description
// @author          Jeppe Rune Mortensen <jepperm@gmail.com>
// @match           https://www.youtube.com/
// @grant           none
// @run-at          document-start
// ==/UserScript==
console.log("[Document Start] " + document.readyState + ", head is " + (document.head ? "" : "not ") + "available");