// ==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 2014.06.29.a
// @author lednerg
// @license (CC) Attribution Non-Commercial Share Alike; http://creativecommons.org/licenses/by-nc-sa/3.0/
// @icon http://i.imgur.com/lslCELP.png
// @include http://*.youtube.com/*
// @include http://youtube.com/*
// @include https://*.youtube.com/*
// @include https://youtube.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @namespace https://greasyfork.org/users/253
// ==/UserScript==
GM_addStyle(""+
".ratingsBar { "+
" transition: height 1s .15s; "+
" height: 4px; "+
" width: 100%; "+
" position: absolute; "+
" bottom: 0px; "+
" } "+
".ratingsBar:hover { "+
" transition: height .5s .15s; "+
" height: 26px; "+
" } "+
".dislikesBar { "+
" height: 100%; "+
" width: 100%; "+
" position: absolute; "+
" right: 0px; "+
" background-color: #CC0000; "+
" } "+
".likesBar { "+
" height: 100%; "+
" position: absolute; "+
" background-color: #00BB22; "+
" } "+
".powerBar { "+
" height: 100%; "+
" position: absolute; "+
" background-color: #0029FF; "+
" background-image: linear-gradient(90deg, transparent 70%, #35B5b9 70%); "+
" background-size: 10px 100%; "+
" } "+
".hatesBar { "+
" height: 100%; "+
" position: absolute; "+
" background-color: #591DD1; "+
" background-image: linear-gradient(90deg, transparent 70%, #C96695 70%); "+
" background-size: 10px 100%; "+
" } "+
".pausedBar { "+
" height: 100%; "+
" position: absolute; "+
" 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; "+
" } "+
".textBar { "+
" transition: opacity .25s .15s; "+
" opacity: 0; "+
" display: flex; "+
" align-items: center; "+
" width: 90%; "+
" height: 16px; "+
" padding: 5px 7.5% 5px 5px; "+
" position: absolute; "+
" bottom: 0px; "+
" margin-bottom: 0px; "+
" color: #f0f0c0; "+
" font-family: arial,sans-serif; "+
" font-size: 11px; "+
" line-height: 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; "+
" } "+
".scanned:hover > .ratingsBar > .textBar, "+
".video-list-item:hover .ratingsBar > .textBar, "+
".feed-item-main-content:hover .ratingsBar > .textBar { "+
" transition: opacity .25s .15s; "+
" opacity: 1; "+
" } "+
".textBar span { "+
" flex: 0 0 auto; "+
" } "+
".textBar div { "+
" display: flex; "+
" flex-flow: row wrap; "+
" flex: 1 1 auto; "+
" } "+
".shadingBar { "+
" transition: opacity 1s .15s; "+
" opacity: 0; "+
" height: 100%; "+
" width: 100%; "+
" position: absolute; "+
" bottom: 0px; "+
" background: linear-gradient( to bottom, rgba(0,0,0,0) 75%, rgba(0,0,0,.2) 90%, rgba(0,0,0,.6) 100% ) ; "+
" } "+
".ratingsBar:hover > .shadingBar { "+
" transition: opacity .5s .35s; "+
" opacity: .85; "+
" } "+
".middleBar, .powerBar, .dislikesBar, .pausedBar { "+
" transition: opacity 1s .0s, outline 1s .0s, outline-offset 1s .0s, border-right 1s .0s; height .5s .5s; "+
" opacity: 1; "+
" outline: 1px solid rgba(0,0,0,.0); "+
" outline-offset: 10px; "+
" } "+
".ratingsBar:hover > .middleBar, .ratingsBar:hover > .powerBar, "+
".ratingsBar:hover > .dislikesBar, .ratingsBar:hover > .pausedBar { "+
" transition: opacity .55s .35s, outline .35s .55s, outline-offset 0s .55s, border-right .35s .55s, height .35s .0s; "+
" opacity: 1; "+
" outline: 1px solid rgba(255,255,255,.75); "+
" outline-offset: 0px; "+
" } "+
".ratingsBar:hover > .dislikesBar, "+
".ratingsBar:hover > .likesBar, "+
".ratingsBar:hover > .pausedBar { "+
"/*transition: height .25s .0s;*/ "+
" height: 90%; "+
" bottom: 0px; "+
" } "+
".video-actions, .video-time { "+
" margin-bottom: 4px; "+
" } "+
".video-actions { "+
" top: 2px; "+
" } "+
".watched .video-thumb { "+
" opacity: 1 !important; "+
" } "+
".watched .video-thumb img { "+
" transition: opacity 1s .25s; "+
" opacity: .5 !important; "+
" } "+
".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 > * { "+
" zoom: .75 !important; "+
" } "+
".playlist-video > .scanned > .ratingsBar > * { "+
" zoom: .8; "+
" } ");
var lastScanTime = new Date().getTime();
scanVideos();
document.onload = function() {
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();
}
};
function scanVideos() {
// 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.ux-thumb-wrap[href^="/watch"] > span.video-thumb:not(.scanned), a.related-video[href^="/watch"] > span:first-child:not(.scanned), a.playlist-video[href^="/watch"] > span.yt-thumb-64:first-child:not(.scanned)');
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);
}
lastScanTime = new Date().getTime();
}
// Parts of this were copied from flux242's old script because I don't understand GM_xmlhttpRequest as well as I should.
// I modified it to get the view count and date. It all seems to work and doesn't throw errors, so... yeah.
function getGdata(node,videoId) {
GM_xmlhttpRequest({
method: 'GET',
url: "http://gdata.youtube.com/feeds/api/videos/" + videoId + "?v=2&alt=json&fields=yt:rating,yt:statistics,published",
onload: function(response) {
if (response.status === 200) {
var rsp = eval( '(' + response.responseText + ')' );
if (rsp && rsp.entry && rsp.entry.published && rsp.entry.yt$statistics && rsp.entry.yt$rating) {
var daysAgo = (lastScanTime - new Date(rsp.entry.published.$t).getTime())/1000/60/60/24;
var views = parseInt(rsp.entry.yt$statistics.viewCount, 10);
var likes = parseInt(rsp.entry.yt$rating.numLikes, 10);
var dislikes = parseInt(rsp.entry.yt$rating.numDislikes, 10);
makeBar(node, daysAgo, views, likes, dislikes);
}
else {
// if there is no data, mark the thumbnail as "scanned" to avoid checking it over and over again
node.classList.add('scanned');
}
}
}
});
}
// the ratings bar is made up of differently colored divs stocked on top of each other
function makeBar(node, daysAgo, views, likes, dislikes) {
// .ratingsBar is for the position (top/bottom) and size of the bar
var container = document.createElement('div');
container.classList.add('ratingsBar');
// barMsg is for the "Power: X%" or "View Count Incorrect" messages for the tooltip
var barMsg = "";
var totalVotes = likes + dislikes;
if (dislikes > 0) {
var dislikesBar = document.createElement('div');
dislikesBar.classList.add('dislikesBar');
container.appendChild(dislikesBar);
}
// Checks to see if the view count has been paused by YouTube. (301-309 views and less than half a day old, or more votes than views)
// We do this because we need an accurate view count to calculate the Power Meter.
// This lets the user know that we can't make one yet, but at least the likesBar/red ratings bar is still available
if (((views > 300) && (views < 310) && (daysAgo <= 0.45)) || (totalVotes > views)) {
if (likes > 0) {
var pausedBar = document.createElement('div');
pausedBar.classList.add('pausedBar');
pausedBar.setAttribute("style","width:"+ (100 * likes / totalVotes) +"%;");
container.appendChild(pausedBar);
}
barMsg = '<div>View Count Paused </div>';
}
else {
powerMeterScore = powerMeter(views, likes);
if (likes > 0) {
var middleBar = document.createElement('div');
middleBar.classList.add('middleBar');
if ((100 * likes / totalVotes) >= powerMeterScore) {
middleBar.classList.add('likesBar');
middleBar.setAttribute("style","width:"+(100 * likes / totalVotes)+"%;");
}
else {
middleBar.classList.add('hatesBar');
middleBar.setAttribute("style","width:"+powerMeterScore+"%;");
}
container.appendChild(middleBar);
}
if (powerMeterScore > 0) {
var powerBar = document.createElement('div');
powerBar.classList.add('powerBar');
if ((100 * likes / totalVotes) > powerMeterScore) {
powerBar.style.width = powerMeterScore+"%";
}
else {
powerBar.style.width = ((100 * likes / totalVotes))+"%";
}
barMsg = '<span><span style="color:#99ddff">'+ Math.round(powerMeterScore*10)/10 +'</span></span> ';
container.appendChild(powerBar);
}
}
// shadingBar gives the bar a 3D look when hovered
var shadingBar = document.createElement('div');
shadingBar.classList.add('shadingBar');
container.appendChild(shadingBar);
// textBar is the div with the numbers on it.
var textBar = document.createElement('div');
textBar.classList.add('textBar');
textBar.innerHTML = '<div>'+ barMsg +'<div">(<span style="color:#77ff77">+'+ likes +' </span>/<span style="color:#ff9977"> -'+ dislikes +'</span>)</div></div>';
container.appendChild(textBar);
node.insertBefore(container,node.childNodes[2]);
node.classList.add('scanned');
}
// trade secrets
function powerMeter(views, likes) {
var viewLikeRatio;
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;
}