// ==UserScript==
// @name YouTube RatingBars (Like/Dislike Rating)
// @namespace knoa.jp
// @description It shows RatingBars whitch represents Like/Dislike Rating ratio.
// @description 動画へのリンクに「高く評価」された比率を示すバーを表示します。
// @include https://www.youtube.com/*
// @version 2.1.0
// @grant none
// @noframes
// en:
// API limits 1M queries/day. (approximately 100 views by 10,000 users.)
// You can use your own APIKEY to support this script.
// https://console.developers.google.com/apis/
// It doesn't support Ajax additional videos yet.
// ja:
// APIの制限は1日あたり100万クエリ(1万ユーザーなら1人あたり100ビュー)です。
// 各自でAPIKEYを書き換えてくれるとスクリプトの寿命が延びます。
// https://console.developers.google.com/apis/
// Ajax追加要素への対応は保留。
// ==/UserScript==
(function () {
const SCRIPTNAME = 'YouTubeRatingBars';
const DEBUG = false;/**/
console.time(SCRIPTNAME);
const HEIGHT = '2px';/*border height*/
const DISLIKECOLOR = 'rgba(136, 136, 136, 0.4)';
const LIKECOLOR = 'rgb(39, 147, 230)';
const MAXRESULTS = 48;/* API limits 50 videos per request */
const APIKEY = 'AIzaSyAyOgssM7s_vvOUDV0ZTRvk6LrTwr_1f5k';
const API = 'https://www.googleapis.com/youtube/v3/videos?id={VIDEOIDS}&part=statistics&fields=items(id,statistics)&maxResults=' + MAXRESULTS + '&key=' + APIKEY;
const NEW = (!document.querySelector('body#body'));
const VIEWS = (NEW) ? {/* querySelectors on each views */
example: ['items', 'anchor[href]', 'insertParent', 'insertAfter'],
home: ['ytd-grid-video-renderer', 'a', '#metadata', '#metadata-line'],
results: ['ytd-video-renderer', 'a', 'ytd-video-meta-block', '#metadata'],
watch: ['ytd-compact-video-renderer', 'a', '#metadata', '#metadata-line'],
} : {
example: ['items', 'anchor[href]', 'insertParent', 'insertAfter'],
home: ['#feed ul > li.yt-shelf-grid-item', 'a', 'div.yt-lockup-content', 'div.yt-lockup-meta'],
results: ['ol.item-section > li', 'div.yt-lockup-video a.yt-uix-tile-link[href]', 'div.yt-lockup-meta', 'ul.yt-lockup-meta-info'],
watch: ['li.video-list-item', 'a.content-link[href]', 'a.content-link', 'span.view-count'],
};
const BARS = (NEW)/* Bar in video pages */
? `<div id="container" class="style-scope ytd-sentiment-bar-renderer" style="background-color: ${DISLIKECOLOR}"><div id="like-bar" class="style-scope ytd-sentiment-bar-renderer" style="width: {LIKES}%;height: ${HEIGHT};background-color: ${LIKECOLOR}"></div></div>`
: `<div class="video-extras-sparkbars"><div class="video-extras-sparkbar-likes" style="width: {LIKES}%;height: ${HEIGHT};"></div><div class="video-extras-sparkbar-dislikes" style="width: {DISLIKES}%;height: ${HEIGHT};"></div></div>`;
let view, items = [], previousContent = '';
let core = {
initialize: function(){
let previousUrl = null;
setInterval(function(){
if(location.href === previousUrl) return;
previousUrl = location.href;
switch(true){
case(location.href === 'https://www.youtube.com/'):
view = VIEWS.home;
break;
case(location.href.startsWith('https://www.youtube.com/results?')):
view = VIEWS.results;
break;
case(location.href.startsWith('https://www.youtube.com/watch?')):
view = VIEWS.watch;
break;
default:
return;
}
items.length = 0;
core.getItems();
}, 1000);
},
getItems: function(){
let previousLength = items.length;
items = document.querySelectorAll(view[0]);
if(items.length === 0) return setTimeout(core.getItems, 1000);
if(items.length !== previousLength) return setTimeout(core.getItems, 1000);/*on loading*/
if(items[0].textContent === previousContent) return setTimeout(core.getItems, 1000);/*not yet replaced content*/
previousContent = items[0].textContent;
setTimeout(core.getBars, 100);
},
getBars: function(){
let videoids = [];
for(let i = 0; items[i]; i++){
try{
let id = items[i].querySelector(view[1]).href.match(/\?v=([^&]+)/)[1];
videoids.push(id);
items[i].dataset.videoid = id;
}catch(e){
continue;
}
}
videoids.length = Math.min(videoids.length, MAXRESULTS);
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', API.replace('{VIDEOIDS}', videoids.join()));
xhr.onreadystatechange = function () {
if(xhr.readyState !== 4 || xhr.status !== 200) return;
if(!xhr.response.items) return;
let bars = {};
for(let i = 0; xhr.response.items[i]; i++){
let v = xhr.response.items[i], s = v.statistics;
if(!s.likeCount && !s.dislikeCount) continue;
bars[v.id] = BARS;
bars[v.id] = bars[v.id].replace('{LIKES}', (100 * parseInt(s.likeCount)) / (parseInt(s.likeCount) + parseInt(s.dislikeCount)));
bars[v.id] = bars[v.id].replace('{DISLIKES}', (100 * parseInt(s.dislikeCount)) / (parseInt(s.likeCount) + parseInt(s.dislikeCount)));
}
for(let i = 0; items[i]; i++){
if(!bars[items[i].dataset.videoid]) continue;
let bar = document.createElement('div');
bar.innerHTML = bars[items[i].dataset.videoid];
bar.id = SCRIPTNAME;
let oldBar = items[i].querySelector('#' + SCRIPTNAME);
if(oldBar){
oldBar.parentNode.replaceChild(bar, oldBar);
}else{
items[i].querySelector(view[2]).insertBefore(bar, items[i].querySelector(view[3]).nextElementSibling);
}
}
};
xhr.send();
},
};
let log = function(){
let l = log.last = log.now || new Date(), n = log.now = new Date();
console.log(
'SCRIPTNAME' + ':',
/* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
/* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
/* :00 */ ':' + new Error().stack.match(/:[0-9]+:[0-9]+/g)[1].split(':')[1],/*LINE*/
/* caller.caller */ (log.caller.caller && log.caller.caller.name ? `${log.caller.caller.name}() => ` : '') +
/* caller */ `${log.caller.name}()`,
...arguments
);
if(arguments.length === 1) return arguments[0];
};
core.initialize();
console.timeEnd(SCRIPTNAME);
})();