lednerg / Youtube Video Ratings Bar with Power Meter

// ==UserScript==
// @name        Youtube Video Ratings Bar with Power Meter
// @description Highlights the most worthwhile videos on YouTube. In addition to a ratings bar, there's also a blue "Power Meter" which measures people's enthusiasm for videos.
// @version     2017.09.14
// @author      lednerg
// @license     (CC) Attribution Non-Commercial Share Alike; http://creativecommons.org/licenses/by-nc-sa/3.0/
// @icon        http://i.imgur.com/ZfKR597.png
// @include     http://*.youtube.com/*
// @include     http://youtube.com/*
// @include     https://*.youtube.com/*
// @include     https://youtube.com/*
// @connect     googleapis.com
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @namespace   https://openuserjs.org/users/lednerg
// @require     http://code.jquery.com/jquery-1.11.1.min.js
// @updateURL   https://openuserjs.org/meta/lednerg/Youtube_Video_Ratings_Bar_with_Power_Meter.meta.js
// ==/UserScript==

GM_addStyle(""+
".resume-playback-background, .resume-playback-progress-bar { "+
"  top: 0px; "+
"  } "+
".resume-playback-progress-bar { "+
"  background: #d29713; "+
"  } "+
".ratingsBar:hover > .likesBar, "+
".ratingsBar:hover > .dislikesBar, "+
".ratingsBar:hover > .pausedBar, "+
".ratingsBar:hover > .powerBar, "+
".ratingsBar:hover > .hatesBar { "+
"  transition: height .25s .0s; "+
"   height: 18px; "+
"  } "+
"                    .likesBar, "+
"                    .dislikesBar, "+
"                    .pausedBar,"+
"                    .ratingsBar, "+
"                    .powerBar, "+
"                    .hatesBar { "+
"  transition: height .25s .0s; "+
"   height: 4px; "+
"  position: absolute; "+
"  bottom: 0px; "+
"  } "+
".ratingsBar:hover { "+
"  transition: height .25s .0s; "+
"   height: 26px; "+
"  }"+
"      .ratingsBar { "+
"  width: 100%; "+
"  z-index: 3; "+
"  } "+
".powerBar,"+
".hatesBar { "+
"  position: absolute; "+
"  top: 0px; "+
"  } "+
".textContainer { "+
"  display: table; "+
"  position: absolute; "+
"  bottom: 0px; "+
"  height: 26px; "+
"  width: 100%; "+
"} "+
".textContainer:hover.short { "+
"  transition: height .15s .0s !important; "+
"   height: 18px; "+
"  padding-top: 8px; "+
"} "+
"      .textContainer.short { "+
"  transition: height .5s .15s; "+
"   height: 26px; "+
"} "+
".dislikesBar { "+
"  width: 100%; "+
"  right: 0px; "+
"  background-color: #CC0000; "+
"  } "+
".likesBar { "+
"  background-color: #00BB22; "+
"  } "+
".powerBar { "+
"  background-color: #0029FF; "+
"  background-position: right; "+
"  background-size: 10px 100%; "+
"  } "+
".hatesBar { "+
"  background-image: linear-gradient(90deg, rgba(200,200,255,.65) 40%, #0029FF 40%); "+
"  background-position: left; "+
"  background-size: 10px 100%; "+
"  } "+
" .pausedBar { "+
"  background-color: #00bb22; "+
"  background-image: linear-gradient(-45deg, #99e449 25%, transparent 25%, transparent 50%, #99e449 50%, #99e449 75%, transparent 75%, transparent); "+
"  background-size: 20px 20px; "+
"  } "+
".yt-uix-simple-thumb-wrap:hover .textBar, "+
"             .video-thumb:hover .textBar { "+
"  transition: opacity .15s .0s; "+
"   opacity: 1; "+
"  } "+
"                                         .textBar { "+
"  transition: opacity .25s .25s; "+
"   opacity: 0; "+
"  display: table-cell; "+
"  position: relative; "+
"  vertical-align: middle; "+
"  width: 100%; "+
"  color: #f0f0c0; "+
"  font-family: arial,​sans-serif; "+
"  font-size: 11px; "+
"  font-weight: 700; "+
"  text-align: left; "+
"  text-shadow: black 0px 0px 7px, black 1px 1px 5px, black 1px 1px 4px, black 1px 1px 3px, black 1px 1px 0px; "+
"  } "+
".textBar:hover > *:hover { "+
"  transition: opacity .25s .15s; "+
"   opacity: .5; "+
"    } "+
".powerScore { "+
"  display: inline-block; "+
"  padding-left: 2px; "+
"  } "+
".ratingsScore { "+
"  display: inline-block; "+
"  padding-left: 2px; "+
"  } "+
".likesScore { "+
"  color: #77ff77; "+
"  } "+
".dislikesScore { "+
"  color: #ff9977; "+
"  padding-right: 2px; "+
"  } "+
".video-extras-sparkbars .ratingsBar { "+
"  bottom: -1.25px !important; "+
"  } "+
".video-extras-sparkbars .ratingsScore { "+
"  display: none !important; "+
"  } "+
".video-extras-sparkbars .textContainer { "+
"  height: 30px !important; "+
"  } "+
".video-extras-sparkbars .textBar { "+
"  transition: opacity .5s 0s; "+
"  bottom: -4px !important; "+
"  } "+
".video-extras-sparkbars:hover .textBar { "+
"  transition: opacity .5s 0s; "+
"  opacity: 1 !important; "+
"  } "+
"#watch7-views-info:hover .watch-view-count { "+
"  transition: opacity .5s 0s; "+
"  opacity: 0 !important; "+
"  } "+
".video-extras-sparkbars .powerScore > span { "+
"  font: 19px 'YouTube Noto',Roboto,arial,sans-serif; "+
"  text-shadow: none !important; "+
"  color: #0029FF !important; "+
"  } "+
".video-extras-sparkbars .powerScore { "+
"  position: absolute !important; "+
"  top: -24px !important; "+
"  left: -3px !important; "+
"  padding-left: 0px !important; "+
"  } "+
".video-extras-sparkbars .textBar:hover > *:hover { "+
"   opacity: 1 !important; "+
"    } "+
".ratingsBar:hover > .shadingBar { "+
"  transition: opacity .25s .15s; "+
"   opacity: .85; "+
"  } "+
"                   .shadingBar { "+
"  transition: opacity .25s .15s; "+
"   opacity: 0; "+
"  height: 100%; "+
"  width: 100%; "+
"  background: linear-gradient( to bottom, rgba(0,0,0,0) 75%, rgba(0,0,0,.2) 90%, rgba(0,0,0,.6) 100% ) ; "+
"  } "+
".video-actions,"+
"   .video-time { "+
"  margin-bottom: 4px; "+
"  } "+
".video-actions { "+
"  top: 2px; "+
"  } "+
".related-list-item:hover .video-time { "+
"  right: -100px; "+
"  } "+
".watched .video-thumb { "+
"  opacity: 1 !important; "+
"  } "+
".watched .video-thumb img { "+
"  transition: opacity 1s .25s; "+
"   opacity: .5 !important; "+
"  -webkit-transform: translate3d( 0px, 0px, 0px ); "+
"  transform: translate3d( 0px, 0px, 0px ); "+
"  } "+
"               .watched:hover .video-thumb img, "+
".feed-item-main-content:hover .video-thumb img { "+
"  transition: opacity .15s 0s; "+
"   opacity: 1 !important; "+
"  } "+
".scanned .yt-thumb-clip { "+
"  bottom: -96px; "+
"  } "+
".scanned .yt-thumb-default { "+
"  margin-bottom: 4px; "+
"  } "+
".yt-thumb-72.scanned > .ratingsBar > *, "+
" .yt-thumb-64.scanned > .ratingsBar > * { "+
"  zoom: .8 !important; "+
"  } "+
".playlist-video > .scanned > .ratingsBar > * { "+
"  zoom: .8; "+
"  } "+
".load-more-button,"+
" .video-list-item { "+
"    animation-duration: 3s; "+
"    -webkit-animation-duration: 3s; "+
"    animation-name: addedThumbnails; "+
"    -webkit-animation-name: addedThumbnails; "+
"    -webkit-animation-iteration-count: 1; "+
"} "+
".yt-pl-thumb .blacklist, .thumb-wrapper .blacklist, .yt-lockup-thumbnail .blacklist { "+
"    right: 26px !important; "+
"    top: 2px !important; "+
"} "+
".yt-pl-thumb .popoutmode, .thumb-wrapper .popoutmode, .yt-lockup-thumbnail .popoutmode { "+
"    bottom: auto !important; "+
"    top: 2px !important; "+
"    left: 2px !important; "+
"} "+
".videowall-still:hover .textBar { "+
"    opacity: 1 !important; "+
"    transition: opacity .25s 0s !important; "+
"} "+
"@keyframes addedThumbnails { "+
"    from { "+
"        outline-color: #0ff; "+
"    } "+
"    to { "+
"        outline-color: #f00; "+
"    } "+
"} "+
"@-webkit-keyframes addedThumbnails {  "+
"    from { "+
"        outline-color: #0ff; "+
"    } "+
"    to { "+
"        outline-color: #f00; "+
"    } "+
"} ");

var lastScanTime = new Date().getTime();

scanVideos();

// On some pages, YouTube adds thumbnails as you scroll down the page,
// so this waits for scroll events and starts the scan for new video thumbnails.
// (it's a bit lazy, and something I want to change later)
window.onscroll = function() {
    var timeNow = new Date().getTime();
    var timeDiff = timeNow - lastScanTime;
    if (timeDiff >= 1000) {
        scanVideos();
    }
};

// Detecting YouTube's SPF processes, which redraw pages without reloading
document.addEventListener("spfprocess", scanVideos);
document.addEventListener("spfdone", scanVideos);

// Detecting Load More button animation
var feedContainer = $(".feed-container, #body-container, #watch-related");
if (feedContainer) { buttonListen(); }
function buttonListen(feedContainer) {
    $("#body-container, .feed-container, #watch-related, .grid-lockups-container").bind("animationstart webkitAnimationStart oAnimationStart MSAnimationStart", function(){ scanVideos();});
    $("#body-container, .feed-container, #watch-related, .grid-lockups-container").bind("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function(){ scanVideos();});
}

function scanVideos() {
    lastScanTime = new Date().getTime();
    // makes a list of video links which are not in the ".scanned" class yet. Once they are scanned, they will be added to it.
    var videoList = document.querySelectorAll('a.yt-uix-sessionlink[href^="/watch"] > .yt-thumb:not(.scanned):not(.gettingData), a.yt-uix-sessionlink[href^="/watch"] > .yt-uix-simple-thumb-wrap:not(.scanned):not(.gettingData)') ;
    var wallList = document.querySelectorAll('a.ytp-videowall-still[href*="/watch"]:not(.scanned):not(.gettingData)');
    var selfList = document.querySelectorAll('div.video-extras-sparkbars:not(.scanned):not(.gettingData)');
    if (videoList.length > 0) {
        for ( var i = 0; i < videoList.length; i++ ) {
            // searches for the video id number which we'll use to poll YouTube for ratings information
            var videoId = videoList[i].parentNode.getAttribute("href").replace(/.*[v|s]=([^&%]*).*/, "$1");
            getGdata(videoList[i],videoId,0);
        }
    }
    // Similar procedure for the post-playback video wall.
    if (wallList.length > 0) {
        for ( var j = 0; j < wallList.length; j++ ) {
            var wallId = wallList[j].getAttribute("href").replace(/.*[v|s]=([^&%]*).*/, "$1");
            wallList[j].classList.add('scanned');
            var wallCont = document.createElement('div');
            wallCont.classList.add('wallCont');
            wallCont = wallList[j].appendChild(wallCont);
            getGdata(wallCont,wallId,0);
        }
    }
    if (selfList.length > 0) {
        var selfId = window.location.href.replace(/.*[v|s]=([^&%]*).*/, "$1");
        getGdata(selfList[0],selfId,1);
    }
}

function getGdata(node,videoId,self) {
    if ( !node.classList.contains("gettingData") ) {
        node.classList.add('gettingData');
        setTimeout(function(){node.classList.toggle("gettingData");},1000);
        GM_xmlhttpRequest({
            method: 'GET',
            url: "https://www.googleapis.com/youtube/v3/videos?id=" + videoId + "&key=AIzaSyBbU7SUrqWYiZPaYIt6fIeMGC5R8rpf02U&part=snippet,statistics&fields=items/statistics,items/snippet/publishedAt",
            onload: function(response) {
                if (response.status === 200) {
                    var rsp = JSON.parse(response.responseText); // Shout out to chose for this
                    if (rsp && rsp.items[0] && rsp.items[0].snippet && rsp.items[0].statistics) {
                        var daysAgo = (lastScanTime - new Date(rsp.items[0].snippet.publishedAt).getTime())/1000/60/60/24;
                        var views = parseInt(rsp.items[0].statistics.viewCount, 10);
                        var likes = parseInt(rsp.items[0].statistics.likeCount, 10);
                        var dislikes = parseInt(rsp.items[0].statistics.dislikeCount, 10);
                        if (isNaN(likes) || isNaN(dislikes)) {
                            views = 0;
                            likes = 0;
                            dislikes = 0;
                        }
                        makeBar(node, daysAgo, views, likes, dislikes, self);
                    }
                }
            }
        });
    }
}

// reformat long numbers for textBar [10,394,235 = "10.4m"]
var pow=Math.pow, floor=Math.floor, abs=Math.abs, log=Math.log;
function round(n, precision) {
    var prec = Math.pow(10, precision);
    return Math.round(n*prec)/prec;
}
function format(n) {
    var base = floor(log(abs(n))/log(1000));
    var suffix = 'kmb'[base-1];
    return suffix ? round(n/pow(1000,base),1)+suffix : ''+n;
}

// the ratings bar is made up of differently colored divs stacked on top of each other
function makeBar(node, daysAgo, views, likes, dislikes, self) {
    var container = document.createElement('div');
    container.classList.add('ratingsBar');
    if (self > 0) { container.classList.add('self'); }
    var barMsg = "";
    var pausedMsg = "";
    var pausedBar = false;
    var totalVotes = likes + dislikes;
    if (dislikes > 0) {
        var dislikesBar = document.createElement('div');
        dislikesBar.classList.add('dislikesBar');
        if (self > 0) { dislikesBar.classList.add('self'); }
        container.appendChild(dislikesBar);
    }
    // Checks to see if there are more votes than views, which would mean the view count is wrong.
    // We do this because we need an accurate view count to calculate the Power Meter.
    // The green/yellow 'pausedBar' lets the user know that we can't make one yet, but at least the likesBar/red ratings bar is still available
    if (totalVotes > views) {
        if (likes > 0) {
            pausedBar = document.createElement('div');
            pausedBar.classList.add('pausedBar');
            if (self > 0) { pausedBar.classList.add('self'); }
            pausedBar.setAttribute("style","width:"+ (100 * likes / totalVotes) +"%;");
            container.appendChild(pausedBar);
        }
        pausedMsg = '<span class="powerScore"><i>&nbsp;View Count Error&nbsp;</i></span>';
    }
    else {
        powerMeterScore = powerMeter(views, likes, dislikes);
        if (likes > 0) {
            var likesBar = document.createElement('div');
            likesBar.classList.add('likesBar');
            if (self > 0) { likesBar.classList.add('self'); }
            likesBar.setAttribute("style","width:"+(100 * likes / totalVotes)+"%;");
            container.appendChild(likesBar);
        }
       // shadingBar gives the ratings bar a 3D look when hovered
       var shadingBar = document.createElement('div');
        if ((likes + dislikes) > 0) {
            shadingBar.classList.add('shadingBar');
            if (self > 0) { shadingBar.classList.add('self'); }
        }
       container.appendChild(shadingBar);
        if ((100 * likes / totalVotes) < powerMeterScore) {
            var hatesBar = document.createElement('div');
            hatesBar.classList.add('hatesBar');
            if (self > 0) { hatesBar.classList.add('self'); }
            hatesBar.setAttribute("style","width:"+(powerMeterScore - (100 * likes / totalVotes))+"%; margin-left: "+(100 * likes / totalVotes)+"%;");
            container.appendChild(hatesBar);
        }
        if (powerMeterScore >= 0.0455) {
            var powerBar = document.createElement('div');
            powerBar.classList.add('powerBar');
            if (self > 0) { powerBar.classList.add('self'); }
            if ((100 * likes / totalVotes) > powerMeterScore) {
                powerBar.style.width = powerMeterScore+"%";
            }
            else {
                powerBar.style.width = ((100 * likes / totalVotes))+"%";
            }
            barMsg = '<span class="powerScore">&nbsp;<span style="color:#99ddff">'+ Math.round(powerMeterScore*10)/10 +'</span>&nbsp;</span>';
            container.appendChild(powerBar);
        }
    }
    if (likes > 0 || dislikes > 0) {
      var textContainer = document.createElement('span');
      textContainer.classList.add('textContainer');
      if (self > 0) { textContainer.classList.add('self'); }
      if (((likes + dislikes) > 0) && (powerMeterScore < 0.0455 || pausedBar)) {textContainer.classList.add('short');}
      var textBar = document.createElement('span');
      textBar.classList.add('textBar');
      if (self > 0) { textBar.classList.add('self'); }
      textBar.innerHTML = barMsg+pausedMsg +'<span class="ratingsScore">&nbsp;<span class="likesScore">+'+ format(likes) +'&nbsp;</span>/<span class="dislikesScore">&nbsp;-'+ format(dislikes) +'</span></span>';
      textContainer.appendChild(textBar);
      container.appendChild(textContainer);
    }
    if ( !node.classList.contains("scanned") ) {
        node.insertBefore(container,node.childNodes[2]);
        node.classList.add('scanned');
    }
}

// trade secrets
function powerMeter(view1, likes, dislikes) {
    var viewLikeRatio;
    var views = view1 - dislikes;
    if (views < 2000) {
        var viewLikeRatio2k = Math.round( (views + views * ((3000-views)/2000)) / (likes) );
        if (views < 255) {
            viewLikeRatio = Math.round( viewLikeRatio2k / (views/255) );
        }
        else {
            viewLikeRatio = viewLikeRatio2k;
        }
    }
    else {
        viewLikeRatio = Math.round( (views+7000) / 3 / (likes) );
    }
    if ((viewLikeRatio < 1) || (viewLikeRatio > 255)) {
        return 0;
    }
    var powerMeterScore = Math.round(Math.pow(((255-viewLikeRatio)/2.55), 3)) / 10000;
    return powerMeterScore;
}