YouTube Restore Dislike Counters

A userscript to restore the dislike counts on YouTube. Not 100% accurate all the time, but stil pretty accurate.

  1. // ==UserScript==
  2. // @name YouTube Restore Dislike Counters
  3. // @version 1.1.0
  4. // @description A userscript to restore the dislike counts on YouTube. Not 100% accurate all the time, but stil pretty accurate.
  5. // @author Kyle Boyd
  6. // @match *://www.youtube.com/*
  7. // @run_at document_start
  8. // @namespace https://greasyfork.org/users/826218
  9. // ==/UserScript==
  10.  
  11. function numberWithCommas(x) {
  12. return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  13. }
  14.  
  15. async function waitForElm(s) {
  16. while (!document.querySelector(s)) {
  17. await new Promise(r => requestAnimationFrame(r))
  18. }
  19. return;
  20. }
  21.  
  22. async function init() {
  23.  
  24. try {
  25.  
  26. var data = document.querySelector("ytd-app").data;
  27.  
  28. for (p = 0; p < data.response.contents.twoColumnWatchNextResults.results.results.contents.length; p++) {
  29.  
  30. if (typeof data.response.contents.twoColumnWatchNextResults.results.results.contents[p].videoPrimaryInfoRenderer != 'undefined') {
  31.  
  32. var vidroot = data.response.contents.twoColumnWatchNextResults.results.results.contents[p];
  33.  
  34. }
  35.  
  36. }
  37.  
  38. if (vidroot.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons[0].toggleButtonRenderer.isToggled) {
  39.  
  40. var l = parseInt(vidroot.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons[0].toggleButtonRenderer.toggledText.accessibility.accessibilityData.label.replace(/( likes|,)/g, ""));
  41.  
  42. } else {
  43.  
  44.  
  45. var l = parseInt(vidroot.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons[0].toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/( likes|,)/g, ""));
  46.  
  47. }
  48. var r = data.playerResponse.videoDetails.averageRating;
  49.  
  50. function calculateDislikes(l, r) {
  51. var d = Math.round(l*((5-r)/(r-1)));
  52. return d;
  53. }
  54.  
  55. if (r != 0) {
  56.  
  57. var dislikes = await calculateDislikes(l, r);
  58.  
  59. } else {
  60.  
  61. var dislikes = 0;
  62.  
  63. }
  64.  
  65. var dislikesfin = numberWithCommas(dislikes)
  66. var likesfin = numberWithCommas(l);
  67. // added bonus
  68.  
  69. if (r != 0) {
  70.  
  71. document.querySelector("yt-formatted-string#text.ytd-toggle-button-renderer").innerHTML = likesfin;
  72.  
  73. } else {
  74.  
  75. document.querySelector("yt-formatted-string#text.ytd-toggle-button-renderer").innerHTML = "0";
  76.  
  77. }
  78.  
  79. document.querySelectorAll("yt-formatted-string#text.ytd-toggle-button-renderer")[1].innerHTML = dislikesfin;
  80.  
  81. var added = l + dislikes;
  82.  
  83. var sentimentPercent = parseInt((((l / added) * 100 * 100) / 100)).toString();
  84.  
  85. document.querySelector("ytd-sentiment-bar-renderer").removeAttribute("hidden");
  86.  
  87. document.getElementById("like-bar").setAttribute("style", "width: " + sentimentPercent + "%;");
  88.  
  89. } catch(e) {};
  90.  
  91. }
  92.  
  93. waitForElm("yt-formatted-string#text.ytd-toggle-button-renderer").then(() => init());
  94.  
  95. window.addEventListener('yt-page-data-updated', init, false);
  96. const LIKED_STATE = "LIKED_STATE";
  97. const DISLIKED_STATE = "DISLIKED_STATE";
  98. const NEUTRAL_STATE = "NEUTRAL_STATE";
  99. let previousState = 3; //1=LIKED, 2=DISLIKED, 3=NEUTRAL
  100. let likesvalue = 0;
  101. let dislikesvalue = 0;
  102.  
  103. let isMobile = location.hostname == "m.youtube.com";
  104. let mobileDislikes = 0;
  105. function cLog(text, subtext = "") {
  106. subtext = subtext.trim() === "" ? "" : `(${subtext})`;
  107. console.log(`[Return YouTube Dislikes] ${text} ${subtext}`);
  108. }
  109.  
  110. function getButtons() {
  111. if (isMobile) {
  112. return document.querySelector(".slim-video-action-bar-actions");
  113. }
  114. if (document.getElementById("menu-container")?.offsetParent === null) {
  115. return document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div");
  116. } else {
  117. return document
  118. .getElementById("menu-container")
  119. ?.querySelector("#top-level-buttons-computed");
  120. }
  121. }
  122.  
  123. function getLikeButton() {
  124. return getButtons().children[0];
  125. }
  126.  
  127. function getDislikeButton() {
  128. return getButtons().children[1];
  129. }
  130.  
  131. function isVideoLiked() {
  132. if (isMobile) {
  133. return (
  134. getLikeButton().querySelector("button").getAttribute("aria-label") ==
  135. "true"
  136. );
  137. }
  138. return getLikeButton().classList.contains("style-default-active");
  139. }
  140.  
  141. function isVideoDisliked() {
  142. if (isMobile) {
  143. return (
  144. getDislikeButton().querySelector("button").getAttribute("aria-label") ==
  145. "true"
  146. );
  147. }
  148. return getDislikeButton().classList.contains("style-default-active");
  149. }
  150.  
  151. function isVideoNotLiked() {
  152. if (isMobile) {
  153. return !isVideoLiked();
  154. }
  155. return getLikeButton().classList.contains("style-text");
  156. }
  157.  
  158. function isVideoNotDisliked() {
  159. if (isMobile) {
  160. return !isVideoDisliked();
  161. }
  162. return getDislikeButton().classList.contains("style-text");
  163. }
  164.  
  165. function checkForUserAvatarButton() {
  166. if (isMobile) {
  167. return;
  168. }
  169. if (document.querySelector('#avatar-btn')) {
  170. return true
  171. } else {
  172. return false
  173. }
  174. }
  175.  
  176. function getState() {
  177. if (isVideoLiked()) {
  178. return LIKED_STATE;
  179. }
  180. if (isVideoDisliked()) {
  181. return DISLIKED_STATE;
  182. }
  183. return NEUTRAL_STATE;
  184. }
  185.  
  186. function setLikes(likesCount) {
  187. if (isMobile) {
  188. getButtons().children[0].querySelector(".button-renderer-text").innerText =
  189. likesCount;
  190. return;
  191. }
  192. getButtons().children[0].querySelector("#text").innerText = likesCount;
  193. }
  194.  
  195. function setDislikes(dislikesCount) {
  196. if (isMobile) {
  197. mobileDislikes = dislikesCount;
  198. return;
  199. }
  200. getButtons().children[1].querySelector("#text").innerText = dislikesCount;
  201. }
  202.  
  203. (typeof GM_addStyle != "undefined"
  204. ? GM_addStyle
  205. : (styles) => {
  206. let styleNode = document.createElement("style");
  207. styleNode.type = "text/css";
  208. styleNode.innerText = styles;
  209. document.head.appendChild(styleNode);
  210. })(`
  211.  
  212.  
  213.  
  214. `);
  215.  
  216. function createRateBar(likes, dislikes) {
  217. if (isMobile) {
  218. return;
  219. }
  220. let rateBar = document.getElementById("return-youtube-dislike-bar-container");
  221.  
  222. const widthPx =
  223. getButtons().children[0].clientWidth +
  224. getButtons().children[1].clientWidth +
  225. 8;
  226.  
  227. const widthPercent =
  228. likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;
  229.  
  230. if (!rateBar) {
  231. document.getElementById("menu-container").insertAdjacentHTML(
  232. "beforeend",
  233. `
  234. <div class="ryd-tooltip" style="width: ${widthPx}px">
  235. <div class="ryd-tooltip-bar-container">
  236. <div
  237. id="return-youtube-dislike-bar-container"
  238. style="width: 100%; height: 2px;"
  239. >
  240. <div
  241. id="return-youtube-dislike-bar"
  242. style="width: ${widthPercent}%; height: 100%"
  243. ></div>
  244. </div>
  245. </div>
  246. <tp-yt-paper-tooltip position="top" id="ryd-dislike-tooltip" class="style-scope ytd-sentiment-bar-renderer" role="tooltip" tabindex="-1">
  247. <!--css-build:shady-->${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}
  248. </tp-yt-paper-tooltip>
  249. </div>
  250. `
  251. );
  252. } else {
  253. document.getElementById(
  254. "return-youtube-dislike-bar-container"
  255. ).style.width = widthPx + "px";
  256. document.getElementById("return-youtube-dislike-bar").style.width =
  257. widthPercent + "%";
  258.  
  259. document.querySelector(
  260. "#ryd-dislike-tooltip > #tooltip"
  261. ).innerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}`;
  262. }
  263. }
  264.  
  265. function setState() {
  266. cLog("Fetching votes...");
  267. let statsSet = false;
  268.  
  269. fetch(
  270. `https://returnyoutubedislikeapi.com/votes?videoId=${getVideoId()}`
  271. ).then((response) => {
  272. response.json().then((json) => {
  273. if (json && !("traceId" in response) && !statsSet) {
  274. const { dislikes, likes } = json;
  275. cLog(`Received count: ${dislikes}`);
  276. likesvalue = likes;
  277. dislikesvalue = dislikes;
  278. setDislikes(numberFormat(dislikes));
  279. createRateBar(likes, dislikes);
  280. }
  281. });
  282. });
  283. }
  284.  
  285. function likeClicked() {
  286. if (checkForUserAvatarButton() == true) {
  287. if (previousState == 1) {
  288. likesvalue--;
  289. createRateBar(likesvalue, dislikesvalue);
  290. setDislikes(numberFormat(dislikesvalue));
  291. previousState = 3
  292. } else if (previousState == 2) {
  293. likesvalue++;
  294. dislikesvalue--;
  295. setDislikes(numberFormat(dislikesvalue))
  296. createRateBar(likesvalue, dislikesvalue);
  297. previousState = 1
  298. } else if (previousState == 3) {
  299. likesvalue++;
  300. createRateBar(likesvalue, dislikesvalue)
  301. previousState = 1
  302. }
  303. }
  304. }
  305.  
  306. function dislikeClicked() {
  307. if (checkForUserAvatarButton() == true) {
  308. if (previousState == 3) {
  309. dislikesvalue++;
  310. setDislikes(numberFormat(dislikesvalue));
  311. createRateBar(likesvalue, dislikesvalue);
  312. previousState = 2
  313. } else if (previousState == 2) {
  314. dislikesvalue--;
  315. setDislikes(numberFormat(dislikesvalue));
  316. createRateBar(likesvalue, dislikesvalue);
  317. previousState = 3
  318. } else if (previousState == 1) {
  319. likesvalue--;
  320. dislikesvalue++;
  321. setDislikes(numberFormat(dislikesvalue));
  322. createRateBar(likesvalue, dislikesvalue);
  323. previousState = 2
  324. }
  325. }
  326. }
  327.  
  328. function setInitialState() {
  329. setState();
  330. }
  331.  
  332. function getVideoId() {
  333. const urlObject = new URL(window.location.href);
  334. const pathname = urlObject.pathname;
  335. if (pathname.startsWith("/clip")) {
  336. return document.querySelector("meta[itemprop='videoId']").content;
  337. } else {
  338. return urlObject.searchParams.get("v");
  339. }
  340. }
  341.  
  342. function isVideoLoaded() {
  343. if (isMobile) {
  344. return document.getElementById("player").getAttribute("loading") == "false";
  345. }
  346. const videoId = getVideoId();
  347.  
  348. return (
  349. document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null
  350. );
  351. }
  352.  
  353. function roundDown(num) {
  354. if (num < 1000) return num;
  355. const int = Math.floor(Math.log10(num) - 2);
  356. const decimal = int + (int % 3 ? 1 : 0);
  357. const value = Math.floor(num / 10 ** decimal);
  358. return value * 10 ** decimal;
  359. }
  360.  
  361. function numberFormat(numberState) {
  362. let localeURL = Array.from(document.querySelectorAll("head > link[rel='search']"))
  363. ?.find((n) => n?.getAttribute("href")?.includes("?locale="))
  364. ?.getAttribute("href");
  365.  
  366. const userLocales = localeURL ? new URL(localeURL)?.searchParams?.get("locale") : document.body.lang;
  367.  
  368. const formatter = Intl.NumberFormat(
  369. document.documentElement.lang || userLocales || navigator.language,
  370. {
  371. notation: "compact",
  372. minimumFractionDigits: 1,
  373. maximumFractionDigits: 1,
  374. }
  375. );
  376.  
  377. return formatter.format(roundDown(numberState)).replace(/\.0|,0/, "");
  378. }
  379.  
  380. function setEventListeners(evt) {
  381. let jsInitChecktimer;
  382.  
  383. function checkForJS_Finish(check) {
  384. console.log();
  385. if (getButtons()?.offsetParent && isVideoLoaded()) {
  386. clearInterval(jsInitChecktimer);
  387. const buttons = getButtons();
  388.  
  389. if (!window.returnDislikeButtonlistenersSet) {
  390. cLog("Registering button listeners...");
  391. buttons.children[0].addEventListener("click", likeClicked);
  392. buttons.children[1].addEventListener("click", dislikeClicked);
  393. window.returnDislikeButtonlistenersSet = true;
  394. }
  395. setInitialState();
  396. }
  397. }
  398.  
  399. if (
  400. window.location.href.indexOf("watch?") >= 0 ||
  401. (isMobile && evt?.indexOf("watch?") >= 0)
  402. ) {
  403. cLog("Setting up...");
  404. jsInitChecktimer = setInterval(checkForJS_Finish, 111);
  405. }
  406. }
  407.  
  408. (function () {
  409. "use strict";
  410. window.addEventListener("yt-navigate-finish", setEventListeners, true);
  411. setEventListeners();
  412. })();
  413. if (isMobile) {
  414. let originalPush = history.pushState;
  415. history.pushState = function (...args) {
  416. window.returnDislikeButtonlistenersSet = false;
  417. setEventListeners(args[2]);
  418. return originalPush.apply(history, args);
  419. };
  420.  
  421.  
  422. setInterval(() => {
  423. getDislikeButton().querySelector(".button-renderer-text").innerText =
  424. mobileDislikes;
  425. }, 1000);
  426. }